Merge remote changes before pushing local changes

Only applies to native CalDAV and native EteSync
pull/1790/head
Alex Baker 4 years ago
parent 39438dd8b7
commit 42e44eafdc

@ -4,7 +4,7 @@ import android.content.Context
import at.bitfire.ical4android.Task.Companion.tasksFromReader import at.bitfire.ical4android.Task.Companion.tasksFromReader
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.tasks.caldav.iCalendar.Companion.applyRemote import org.tasks.caldav.applyRemote
import org.tasks.caldav.iCalendar.Companion.reminders import org.tasks.caldav.iCalendar.Companion.reminders
import org.tasks.data.Alarm import org.tasks.data.Alarm
import org.tasks.data.CaldavTask import org.tasks.data.CaldavTask
@ -42,7 +42,7 @@ object TestUtilities {
fun vtodo(path: String): Task { fun vtodo(path: String): Task {
val task = Task() val task = Task()
task.applyRemote(fromResource(path)) task.applyRemote(fromResource(path), null)
return task return task
} }
@ -53,7 +53,7 @@ object TestUtilities {
val task = Task() val task = Task()
val vtodo = readFile(path) val vtodo = readFile(path)
val remote = fromString(vtodo) val remote = fromString(vtodo)
task.applyRemote(remote) task.applyRemote(remote, null)
return Triple(task, CaldavTask().apply { this.vtodo = vtodo }, remote) return Triple(task, CaldavTask().apply { this.vtodo = vtodo }, remote)
} }

@ -6,6 +6,7 @@ import com.natpryce.makeiteasy.Property.newProperty
import com.natpryce.makeiteasy.PropertyLookup import com.natpryce.makeiteasy.PropertyLookup
import com.natpryce.makeiteasy.PropertyValue import com.natpryce.makeiteasy.PropertyValue
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.NO_UUID import com.todoroo.astrid.data.Task.Companion.NO_UUID
import org.tasks.Strings import org.tasks.Strings
import org.tasks.date.DateTimeUtils import org.tasks.date.DateTimeUtils
@ -16,6 +17,7 @@ object TaskMaker {
val ID: Property<Task, Long> = newProperty() val ID: Property<Task, Long> = newProperty()
val DUE_DATE: Property<Task, DateTime?> = newProperty() val DUE_DATE: Property<Task, DateTime?> = newProperty()
val DUE_TIME: Property<Task, DateTime?> = newProperty() val DUE_TIME: Property<Task, DateTime?> = newProperty()
val START_DATE: Property<Task, DateTime?> = newProperty()
val REMINDER_LAST: Property<Task, DateTime?> = newProperty() val REMINDER_LAST: Property<Task, DateTime?> = newProperty()
val HIDE_TYPE: Property<Task, Int> = newProperty() val HIDE_TYPE: Property<Task, Int> = newProperty()
val REMINDERS: Property<Task, Int> = newProperty() val REMINDERS: Property<Task, Int> = newProperty()
@ -30,6 +32,7 @@ object TaskMaker {
val PARENT: Property<Task, Long> = newProperty() val PARENT: Property<Task, Long> = newProperty()
val UUID: Property<Task, String> = newProperty() val UUID: Property<Task, String> = newProperty()
val COLLAPSED: Property<Task, Boolean> = newProperty() val COLLAPSED: Property<Task, Boolean> = newProperty()
val DESCRIPTION: Property<Task, String?> = newProperty()
private val instantiator = Instantiator { lookup: PropertyLookup<Task> -> private val instantiator = Instantiator { lookup: PropertyLookup<Task> ->
val task = Task() val task = Task()
@ -61,6 +64,9 @@ object TaskMaker {
if (deletedTime != null) { if (deletedTime != null) {
task.deletionDate = deletedTime.millis task.deletionDate = deletedTime.millis
} }
lookup.valueOf(START_DATE, null as DateTime?)?.let {
task.hideUntil = task.createHideUntil(HIDE_UNTIL_SPECIFIC_DAY, it.millis)
}
val hideType = lookup.valueOf(HIDE_TYPE, -1) val hideType = lookup.valueOf(HIDE_TYPE, -1)
if (hideType >= 0) { if (hideType >= 0) {
task.hideUntil = task.createHideUntil(hideType, 0) task.hideUntil = task.createHideUntil(hideType, 0)
@ -76,6 +82,7 @@ object TaskMaker {
lookup.valueOf(RECUR, null as String?)?.let { lookup.valueOf(RECUR, null as String?)?.let {
task.setRecurrence(it, lookup.valueOf(AFTER_COMPLETE, false)) task.setRecurrence(it, lookup.valueOf(AFTER_COMPLETE, false))
} }
task.notes = lookup.valueOf(DESCRIPTION, null as String?)
task.isCollapsed = lookup.valueOf(COLLAPSED, false) task.isCollapsed = lookup.valueOf(COLLAPSED, false)
task.uuid = lookup.valueOf(UUID, NO_UUID) task.uuid = lookup.valueOf(UUID, NO_UUID)
val creationTime = lookup.valueOf(CREATION_TIME, DateTimeUtils.newDateTime()) val creationTime = lookup.valueOf(CREATION_TIME, DateTimeUtils.newDateTime())

@ -0,0 +1,73 @@
package org.tasks.makers
import at.bitfire.ical4android.Task
import com.natpryce.makeiteasy.Instantiator
import com.natpryce.makeiteasy.Property
import com.natpryce.makeiteasy.Property.newProperty
import com.natpryce.makeiteasy.PropertyLookup
import com.natpryce.makeiteasy.PropertyValue
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.Priority
import net.fortuna.ical4j.model.property.RRule
import net.fortuna.ical4j.model.property.Status
import org.tasks.caldav.iCalendar
import org.tasks.caldav.iCalendar.Companion.collapsed
import org.tasks.caldav.iCalendar.Companion.order
import org.tasks.caldav.iCalendar.Companion.parent
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils.toDate
@Suppress("ClassName")
object iCalMaker {
val TITLE: Property<Task, String?> = newProperty()
val DESCRIPTION: Property<Task, String?> = newProperty()
val DUE_DATE: Property<Task, DateTime?> = newProperty()
val DUE_TIME: Property<Task, DateTime?> = newProperty()
val START_DATE: Property<Task, DateTime?> = newProperty()
val START_TIME: Property<Task, DateTime?> = newProperty()
val CREATED_AT: Property<Task, DateTime?> = newProperty()
val COMPLETED_AT: Property<Task, DateTime?> = newProperty()
val ORDER: Property<Task, Long?> = newProperty()
val PARENT: Property<Task, String?> = newProperty()
val PRIORITY: Property<Task, Int> = newProperty()
val COLLAPSED: Property<Task, Boolean> = newProperty()
val RRULE: Property<Task, String?> = newProperty()
val STATUS: Property<Task, Status?> = newProperty()
private val instantiator = Instantiator { lookup: PropertyLookup<Task> ->
val task = Task()
lookup.valueOf(CREATED_AT, null as DateTime?)?.let {
task.createdAt = it.millis
}
lookup.valueOf(DUE_DATE, null as DateTime?)?.let {
task.due = Due(it.millis.toDate())
}
lookup.valueOf(DUE_TIME, null as DateTime?)?.let {
task.due = Due(iCalendar.getDateTime(it.millis))
}
lookup.valueOf(START_DATE, null as DateTime?)?.let {
task.dtStart = DtStart(it.millis.toDate())
}
lookup.valueOf(START_TIME, null as DateTime?)?.let {
task.dtStart = DtStart(iCalendar.getDateTime(it.millis))
}
lookup.valueOf(COMPLETED_AT, null as DateTime?)?.let {
task.completedAt = Completed(iCalendar.getDateTime(it.millis))
task.status = Status.VTODO_COMPLETED
}
task.order = lookup.valueOf(ORDER, null as Long?)
task.summary = lookup.valueOf(TITLE, null as String?)
task.parent = lookup.valueOf(PARENT, null as String?)
task.description = lookup.valueOf(DESCRIPTION, null as String?)
task.priority = lookup.valueOf(PRIORITY, Priority.UNDEFINED.level)
task.collapsed = lookup.valueOf(COLLAPSED, false)
task.rRule = lookup.valueOf(RRULE, null as String?)?.let { RRule(it) }
task.status = lookup.valueOf(STATUS, null as Status?)
task
}
fun newIcal(vararg properties: PropertyValue<in Task?, *>): Task {
return Maker.make(instantiator, *properties)
}
}

@ -196,7 +196,10 @@ class CaldavSynchronizer @Inject constructor(
resource resource
.principals(account, calendar) .principals(account, calendar)
.let { principalDao.deleteRemoved(calendar.id, it.map(PrincipalAccess::id)) } .let { principalDao.deleteRemoved(calendar.id, it.map(PrincipalAccess::id)) }
sync(calendar, resource, caldavClient.httpClient) fetchChanges(calendar, resource, caldavClient.httpClient)
if (calendar.access != ACCESS_READ_ONLY) {
pushLocalChanges(calendar, caldavClient.httpClient, resource.href)
}
} }
setError(account, "") setError(account, "")
} }
@ -218,20 +221,17 @@ class CaldavSynchronizer @Inject constructor(
} }
} }
private suspend fun sync( private suspend fun fetchChanges(
caldavCalendar: CaldavCalendar, caldavCalendar: CaldavCalendar,
resource: Response, resource: Response,
httpClient: OkHttpClient) { httpClient: OkHttpClient) {
Timber.d("sync(%s)", caldavCalendar)
val httpUrl = resource.href val httpUrl = resource.href
if (caldavCalendar.access != ACCESS_READ_ONLY) {
pushLocalChanges(caldavCalendar, httpClient, httpUrl)
}
val remoteCtag = resource.ctag val remoteCtag = resource.ctag
if (caldavCalendar.ctag?.equals(remoteCtag) == true) { if (caldavCalendar.ctag?.equals(remoteCtag) == true) {
Timber.d("%s up to date", caldavCalendar.name) Timber.d("%s up to date", caldavCalendar.name)
return return
} }
Timber.d("updating $caldavCalendar")
val davCalendar = DavCalendar(httpClient, httpUrl) val davCalendar = DavCalendar(httpClient, httpUrl)
val members = ArrayList<Response>() val members = ArrayList<Response>()
davCalendar.calendarQuery("VTODO", null, null) { response, relation -> davCalendar.calendarQuery("VTODO", null, null) { response, relation ->

@ -2,15 +2,14 @@ package org.tasks.caldav
import net.fortuna.ical4j.model.property.Geo import net.fortuna.ical4j.model.property.Geo
import org.tasks.data.Location import org.tasks.data.Location
import org.tasks.data.Place
import java.math.BigDecimal import java.math.BigDecimal
import kotlin.math.min import kotlin.math.min
object GeoUtils { object GeoUtils {
fun toGeo(location: Location?) = if (location == null) { fun toGeo(location: Location?) = location?.place?.toGeo()
null
} else { fun Place.toGeo() = Geo("$latitude;$longitude")
Geo("${location.latitude};${location.longitude}")
}
fun Geo.latitudeLike() = latitude.toLikeString() fun Geo.latitudeLike() = latitude.toLikeString()

@ -3,7 +3,6 @@ package org.tasks.caldav
import at.bitfire.ical4android.DateUtils.ical4jTimeZone import at.bitfire.ical4android.DateUtils.ical4jTimeZone
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.andlib.utility.DateUtilities
import com.todoroo.astrid.alarms.AlarmService import com.todoroo.astrid.alarms.AlarmService
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
@ -17,7 +16,6 @@ import net.fortuna.ical4j.model.Parameter
import net.fortuna.ical4j.model.Property import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.component.VAlarm import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.parameter.RelType import net.fortuna.ical4j.model.parameter.RelType
import net.fortuna.ical4j.model.parameter.Related.*
import net.fortuna.ical4j.model.property.Action import net.fortuna.ical4j.model.property.Action
import net.fortuna.ical4j.model.property.Completed import net.fortuna.ical4j.model.property.Completed
import net.fortuna.ical4j.model.property.DateProperty import net.fortuna.ical4j.model.property.DateProperty
@ -53,7 +51,6 @@ import org.tasks.location.GeofenceApi
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.repeats.RecurrenceUtils.newRRule import org.tasks.repeats.RecurrenceUtils.newRRule
import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.time.DateTime.UTC
import org.tasks.time.DateTimeUtils.startOfDay import org.tasks.time.DateTimeUtils.startOfDay
import org.tasks.time.DateTimeUtils.startOfMinute import org.tasks.time.DateTimeUtils.startOfMinute
import org.tasks.time.DateTimeUtils.toDate import org.tasks.time.DateTimeUtils.toDate
@ -139,14 +136,10 @@ class iCalendar @Inject constructor(
remoteModel = Task() remoteModel = Task()
} }
toVtodo(caldavTask, task, remoteModel) return toVtodo(caldavTask, task, remoteModel)
val os = ByteArrayOutputStream()
remoteModel.write(os)
return os.toByteArray()
} }
suspend fun toVtodo(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task, remoteModel: Task) { suspend fun toVtodo(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task, remoteModel: Task): ByteArray {
remoteModel.applyLocal(caldavTask, task) remoteModel.applyLocal(caldavTask, task)
val categories = remoteModel.categories val categories = remoteModel.categories
categories.clear() categories.clear()
@ -167,6 +160,9 @@ class iCalendar @Inject constructor(
val alarms = alarmDao.getAlarms(task.id) val alarms = alarmDao.getAlarms(task.id)
remoteModel.snooze = alarms.find { it.type == TYPE_SNOOZE }?.time remoteModel.snooze = alarms.find { it.type == TYPE_SNOOZE }?.time
remoteModel.alarms.addAll(alarms.toVAlarms()) remoteModel.alarms.addAll(alarms.toVAlarms())
val os = ByteArrayOutputStream()
remoteModel.write(os)
return os.toByteArray()
} }
suspend fun fromVtodo( suspend fun fromVtodo(
@ -175,29 +171,47 @@ class iCalendar @Inject constructor(
remote: Task, remote: Task,
vtodo: String?, vtodo: String?,
obj: String? = null, obj: String? = null,
eTag: String? = null) { eTag: String? = null
) {
val task = existing?.task?.let { taskDao.fetch(it) } val task = existing?.task?.let { taskDao.fetch(it) }
?: taskCreator.createWithValues("").apply { ?: taskCreator.createWithValues("").apply {
taskDao.createNew(this) taskDao.createNew(this)
existing?.task = id existing?.task = id
} }
val caldavTask = existing ?: CaldavTask(task.id, calendar.uuid, remote.uid, obj) val caldavTask = existing ?: CaldavTask(task.id, calendar.uuid, remote.uid, obj)
task.applyRemote(remote) val dirty = task.modificationDate > caldavTask.lastSync || caldavTask.lastSync == 0L
val local = caldavTask.vtodo?.let { fromVtodo(it) }
task.applyRemote(remote, local)
caldavTask.applyRemote(remote, local)
val place = locationDao.getPlaceForTask(task.id)
if (place?.toGeo() == local?.geoPosition) {
setPlace(task.id, remote.geoPosition) setPlace(task.id, remote.geoPosition)
}
val tags = tagDataDao.getTagDataForTask(task.id)
val localTags = getTags(local?.categories ?: emptyList())
if (tags.toSet() == localTags.toSet()) {
tagDao.applyTags(task, tagDataDao, getTags(remote.categories)) tagDao.applyTags(task, tagDataDao, getTags(remote.categories))
val randomReminders = alarmDao.getAlarms(task.id).filter { it.type == TYPE_RANDOM } }
alarmService.synchronizeAlarms(
caldavTask.task, val alarms = alarmDao.getAlarms(task.id)
remote.reminders.plus(randomReminders).toMutableSet() val randomReminders = alarms.filter { it.type == TYPE_RANDOM }
) val localReminders =
local?.reminders?.plus(randomReminders) ?: randomReminders
if (alarms.toSet() == localReminders.toSet()) {
val remoteReminders = remote.reminders.plus(randomReminders)
alarmService.synchronizeAlarms(caldavTask.task, remoteReminders.toMutableSet())
}
task.suppressSync() task.suppressSync()
task.suppressRefresh() task.suppressRefresh()
taskDao.save(task) taskDao.save(task)
caldavTask.vtodo = vtodo caldavTask.vtodo = vtodo
caldavTask.etag = eTag caldavTask.etag = eTag
if (!dirty) {
caldavTask.lastSync = task.modificationDate caldavTask.lastSync = task.modificationDate
caldavTask.remoteParent = remote.parent }
caldavTask.order = remote.order
if (caldavTask.id == com.todoroo.astrid.data.Task.NO_ID) { if (caldavTask.id == com.todoroo.astrid.data.Task.NO_ID) {
caldavTask.id = caldavDao.insert(caldavTask) caldavTask.id = caldavDao.insert(caldavTask)
Timber.d("NEW %s", caldavTask) Timber.d("NEW %s", caldavTask)
@ -227,7 +241,11 @@ class iCalendar @Inject constructor(
private val IS_MOZ_LASTACK = { x: Property? -> x?.name.equals(MOZ_LASTACK, true) } private val IS_MOZ_LASTACK = { x: Property? -> x?.name.equals(MOZ_LASTACK, true) }
fun Due?.apply(task: com.todoroo.astrid.data.Task) { fun Due?.apply(task: com.todoroo.astrid.data.Task) {
task.dueDate = when (this?.date) { task.dueDate = toMillis()
}
fun Due?.toMillis() =
when (this?.date) {
null -> 0 null -> 0
is DateTime -> com.todoroo.astrid.data.Task.createDueDate( is DateTime -> com.todoroo.astrid.data.Task.createDueDate(
URGENCY_SPECIFIC_DAY_TIME, URGENCY_SPECIFIC_DAY_TIME,
@ -238,15 +256,17 @@ class iCalendar @Inject constructor(
getLocal(this) getLocal(this)
) )
} }
}
fun DtStart?.apply(task: com.todoroo.astrid.data.Task) { fun DtStart?.apply(task: com.todoroo.astrid.data.Task) {
task.hideUntil = when (this?.date) { task.hideUntil = toMillis(task)
}
fun DtStart?.toMillis(task: com.todoroo.astrid.data.Task) =
when (this?.date) {
null -> 0 null -> 0
is DateTime -> task.createHideUntil(HIDE_UNTIL_SPECIFIC_DAY_TIME, getLocal(this)) is DateTime -> task.createHideUntil(HIDE_UNTIL_SPECIFIC_DAY_TIME, getLocal(this))
else -> task.createHideUntil(HIDE_UNTIL_SPECIFIC_DAY, getLocal(this)) else -> task.createHideUntil(HIDE_UNTIL_SPECIFIC_DAY, getLocal(this))
} }
}
internal fun getLocal(property: DateProperty): Long = internal fun getLocal(property: DateProperty): Long =
org.tasks.time.DateTime.from(property.date)?.toLocal()?.millis ?: 0 org.tasks.time.DateTime.from(property.date)?.toLocal()?.millis ?: 0
@ -334,35 +354,6 @@ class iCalendar @Inject constructor(
?: unknownProperties.removeIf(IS_MOZ_SNOOZE_TIME) ?: unknownProperties.removeIf(IS_MOZ_SNOOZE_TIME)
} }
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?.recur)
remote.due.apply(this)
remote.dtStart.apply(this)
isCollapsed = remote.collapsed
}
fun Task.applyLocal(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task) { fun Task.applyLocal(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task) {
createdAt = newDateTime(task.creationDate).toUTC().millis createdAt = newDateTime(task.creationDate).toUTC().millis
summary = task.title summary = task.title

@ -0,0 +1,132 @@
package org.tasks.caldav
import at.bitfire.ical4android.Task
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task.Companion.withoutFrom
import com.todoroo.astrid.data.Task.Priority.Companion.HIGH
import com.todoroo.astrid.data.Task.Priority.Companion.LOW
import com.todoroo.astrid.data.Task.Priority.Companion.MEDIUM
import com.todoroo.astrid.data.Task.Priority.Companion.NONE
import net.fortuna.ical4j.model.property.Status
import org.tasks.caldav.iCalendar.Companion.collapsed
import org.tasks.caldav.iCalendar.Companion.getLocal
import org.tasks.caldav.iCalendar.Companion.order
import org.tasks.caldav.iCalendar.Companion.parent
import org.tasks.caldav.iCalendar.Companion.toMillis
import org.tasks.data.CaldavTask
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.time.DateTime.UTC
import org.tasks.time.DateTimeUtils.startOfMinute
import org.tasks.time.DateTimeUtils.startOfSecond
fun com.todoroo.astrid.data.Task.applyRemote(
remote: Task,
local: Task?
): com.todoroo.astrid.data.Task {
applyCompletedAt(remote, local)
applyCreatedAt(remote, local)
applyTitle(remote, local)
applyDescription(remote, local)
applyPriority(remote, local)
applyRecurrence(remote, local)
applyDue(remote, local)
applyStart(remote, local)
applyCollapsed(remote, local)
return this
}
fun CaldavTask.applyRemote(remote: Task, local: Task?): CaldavTask {
applyParent(remote, local)
applyOrder(remote, local)
return this
}
private fun com.todoroo.astrid.data.Task.applyCompletedAt(remote: Task, local: Task?) {
if (local == null ||
(local.completedAt?.let { getLocal(it) } ?: 0) == completionDate.startOfSecond() &&
(local.status == Status.VTODO_COMPLETED) == isCompleted
) {
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
}
}
}
private fun com.todoroo.astrid.data.Task.applyCreatedAt(remote: Task, local: Task?) {
val localCreated = local?.createdAt?.let { newDateTime(it, UTC) }?.toLocal()?.millis
if (localCreated == null || localCreated == creationDate) {
remote.createdAt?.let {
creationDate = newDateTime(it, UTC).toLocal().millis
}
}
}
private fun com.todoroo.astrid.data.Task.applyTitle(remote: Task, local: Task?) {
if (local == null || local.summary == title) {
title = remote.summary
}
}
private fun com.todoroo.astrid.data.Task.applyDescription(remote: Task, local: Task?) {
if (local == null || local.description == notes) {
notes = remote.description
}
}
private fun com.todoroo.astrid.data.Task.applyPriority(remote: Task, local: Task?) {
if (local == null || local.tasksPriority == priority) {
priority = remote.tasksPriority
}
}
private fun com.todoroo.astrid.data.Task.applyRecurrence(remote: Task, local: Task?) {
if (local == null || local.rRule?.recur?.toString() == recurrence.withoutFrom()) {
setRecurrence(remote.rRule?.recur)
}
}
private fun com.todoroo.astrid.data.Task.applyDue(remote: Task, local: Task?) {
if (local == null || local.due.toMillis() == dueDate) {
dueDate = remote.due.toMillis()
}
}
private fun com.todoroo.astrid.data.Task.applyStart(remote: Task, local: Task?) {
if (local == null || local.dtStart.toMillis(this) == hideUntil) {
hideUntil = remote.dtStart.toMillis(this)
}
}
private fun com.todoroo.astrid.data.Task.applyCollapsed(remote: Task, local: Task?) {
if (local == null || isCollapsed == local.collapsed) {
isCollapsed = remote.collapsed
}
}
private fun CaldavTask.applyOrder(remote: Task, local: Task?) {
if (local == null || local.order == order) {
order = remote.order
}
}
private fun CaldavTask.applyParent(remote: Task, local: Task?) {
if (local == null || local.parent == remoteParent) {
remoteParent = remote.parent
}
}
private val Task.tasksPriority: Int
get() = when (this.priority) {
// https://tools.ietf.org/html/rfc5545#section-3.8.1.9
in 1..4 -> HIGH
5 -> MEDIUM
in 6..9 -> LOW
else -> NONE
}

@ -5,7 +5,11 @@ import android.graphics.Color
import at.bitfire.ical4android.ICalendar.Companion.prodId import at.bitfire.ical4android.ICalendar.Companion.prodId
import com.etebase.client.Collection import com.etebase.client.Collection
import com.etebase.client.Item import com.etebase.client.Item
import com.etebase.client.exceptions.* import com.etebase.client.exceptions.ConnectionException
import com.etebase.client.exceptions.PermissionDeniedException
import com.etebase.client.exceptions.ServerErrorException
import com.etebase.client.exceptions.TemporaryServerErrorException
import com.etebase.client.exceptions.UnauthorizedException
import com.todoroo.astrid.helper.UUIDHelper import com.todoroo.astrid.helper.UUIDHelper
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
@ -22,7 +26,6 @@ import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao import org.tasks.data.CaldavDao
import org.tasks.time.DateTimeUtils.currentTimeMillis import org.tasks.time.DateTimeUtils.currentTimeMillis
import timber.log.Timber import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
class EtebaseSynchronizer @Inject constructor( class EtebaseSynchronizer @Inject constructor(
@ -92,7 +95,8 @@ class EtebaseSynchronizer @Inject constructor(
caldavDao.update(calendar) caldavDao.update(calendar)
localBroadcastManager.broadcastRefreshList() localBroadcastManager.broadcastRefreshList()
} }
sync(client, calendar, collection) fetchChanges(client, calendar, collection)
pushLocalChanges(client, calendar, collection)
} }
setError(account, "") setError(account, "")
} }
@ -109,18 +113,16 @@ class EtebaseSynchronizer @Inject constructor(
} }
} }
private suspend fun sync( private suspend fun fetchChanges(
client: EtebaseClient, client: EtebaseClient,
caldavCalendar: CaldavCalendar, caldavCalendar: CaldavCalendar,
collection: Collection collection: Collection
) { ) {
Timber.d("sync(%s)", caldavCalendar) if (caldavCalendar.ctag?.equals(collection.stoken) == true) {
pushLocalChanges(client, caldavCalendar, collection)
val localCtag = caldavCalendar.ctag
if (localCtag != null && localCtag == collection.stoken) {
Timber.d("${caldavCalendar.name} up to date") Timber.d("${caldavCalendar.name} up to date")
return return
} }
Timber.d("updating $caldavCalendar")
client.fetchItems(collection, caldavCalendar) { (stoken, items) -> client.fetchItems(collection, caldavCalendar) { (stoken, items) ->
applyEntries(caldavCalendar, items, stoken) applyEntries(caldavCalendar, items, stoken)
client.updateCache(collection, items) client.updateCache(collection, items)

@ -15,14 +15,18 @@ import org.tasks.analytics.Constants
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.caldav.iCalendar import org.tasks.caldav.iCalendar
import org.tasks.data.* import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask
import org.tasks.data.MyAndroidTask
import org.tasks.data.OpenTaskDao
import org.tasks.data.OpenTaskDao.Companion.isDavx5 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.data.OpenTaskDao.Companion.toLocalCalendar import org.tasks.data.OpenTaskDao.Companion.toLocalCalendar
import timber.log.Timber import timber.log.Timber
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -86,7 +90,9 @@ class OpenTasksSynchronizer @Inject constructor(
.forEach { taskDeleter.delete(it) } .forEach { taskDeleter.delete(it) }
lists.forEach { lists.forEach {
val calendar = toLocalCalendar(it) val calendar = toLocalCalendar(it)
sync(account, calendar, it.ctag, it.id) val isEteSync = account.uuid?.isEteSync() == true
pushChanges(isEteSync, calendar, it.id)
fetchChanges(isEteSync, calendar, it.ctag, it.id)
} }
} }
@ -107,15 +113,7 @@ class OpenTasksSynchronizer @Inject constructor(
return local return local
} }
private suspend fun sync( private suspend fun pushChanges(isEteSync: Boolean, calendar: CaldavCalendar, listId: Long) {
account: CaldavAccount,
calendar: CaldavCalendar,
ctag: String?,
listId: Long
) {
Timber.d("SYNC $calendar")
val isEteSync = account.uuid?.isEteSync() == true
val moved = caldavDao.getMoved(calendar.uuid!!) val moved = caldavDao.getMoved(calendar.uuid!!)
val (deleted, updated) = taskDao val (deleted, updated) = taskDao
.getCaldavTasksToPush(calendar.uuid!!) .getCaldavTasksToPush(calendar.uuid!!)
@ -131,13 +129,19 @@ class OpenTasksSynchronizer @Inject constructor(
updated.forEach { updated.forEach {
push(it, listId, isEteSync) push(it, listId, isEteSync)
} }
}
ctag?.let { private suspend fun fetchChanges(
if (ctag == calendar.ctag) { isEteSync: Boolean,
calendar: CaldavCalendar,
ctag: String?,
listId: Long
) {
if (calendar.ctag?.equals(ctag) == true) {
Timber.d("UP TO DATE: $calendar") Timber.d("UP TO DATE: $calendar")
return@sync return
}
} }
Timber.d("SYNC $calendar")
val etags = openTaskDao.getEtags(listId) val etags = openTaskDao.getEtags(listId)
etags.forEach { (uid, sync1, version) -> etags.forEach { (uid, sync1, version) ->

@ -134,6 +134,12 @@ public class DateTime {
return new DateTime(calendar); return new DateTime(calendar);
} }
public DateTime startOfSecond() {
Calendar calendar = getCalendar();
calendar.set(Calendar.MILLISECOND, 0);
return new DateTime(calendar);
}
public DateTime endOfMinute() { public DateTime endOfMinute() {
Calendar calendar = getCalendar(); Calendar calendar = getCalendar();
calendar.set(Calendar.SECOND, 59); calendar.set(Calendar.SECOND, 59);

@ -40,6 +40,8 @@ object DateTimeUtils {
fun Long.startOfMinute(): Long = if (this > 0) toDateTime().startOfMinute().millis else 0 fun Long.startOfMinute(): Long = if (this > 0) toDateTime().startOfMinute().millis else 0
fun Long.startOfSecond(): Long = if (this > 0) toDateTime().startOfSecond().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
fun Long.toDate(): net.fortuna.ical4j.model.Date? = this.toDateTime().toDate() fun Long.toDate(): net.fortuna.ical4j.model.Date? = this.toDateTime().toDate()

@ -0,0 +1,660 @@
@file:Suppress("ClassName")
package org.tasks.caldav
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY
import com.todoroo.astrid.data.Task.Companion.createDueDate
import com.todoroo.astrid.data.Task.Priority.Companion.HIGH
import com.todoroo.astrid.data.Task.Priority.Companion.LOW
import com.todoroo.astrid.data.Task.Priority.Companion.MEDIUM
import net.fortuna.ical4j.model.property.Status
import org.junit.Assert.*
import org.junit.Test
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.makers.CaldavTaskMaker.REMOTE_ORDER
import org.tasks.makers.CaldavTaskMaker.REMOTE_PARENT
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.COMPLETION_TIME
import org.tasks.makers.TaskMaker.CREATION_TIME
import org.tasks.makers.TaskMaker.newTask
import org.tasks.makers.iCalMaker.COLLAPSED
import org.tasks.makers.iCalMaker.COMPLETED_AT
import org.tasks.makers.iCalMaker.CREATED_AT
import org.tasks.makers.iCalMaker.DESCRIPTION
import org.tasks.makers.iCalMaker.DUE_DATE
import org.tasks.makers.iCalMaker.ORDER
import org.tasks.makers.iCalMaker.PARENT
import org.tasks.makers.iCalMaker.PRIORITY
import org.tasks.makers.iCalMaker.RRULE
import org.tasks.makers.iCalMaker.START_DATE
import org.tasks.makers.iCalMaker.STATUS
import org.tasks.makers.iCalMaker.TITLE
import org.tasks.makers.iCalMaker.newIcal
import org.tasks.time.DateTime
class iCalendarMergeTest {
@Test
fun applyTitleNewTask() =
newTask()
.applyRemote(
remote = newIcal(with(TITLE, "Title")),
local = null
)
.let {
assertEquals("Title", it.title)
}
@Test
fun remoteUpdatedTitle() =
newTask(with(TaskMaker.TITLE, "Title"))
.applyRemote(
remote = newIcal(with(TITLE, "Title2")),
local = newIcal(with(TITLE, "Title")),
)
.let {
assertEquals("Title2", it.title)
}
@Test
fun localBeatsRemoteTitle() =
newTask(with(TaskMaker.TITLE, "Title3"))
.applyRemote(
remote = newIcal(with(TITLE, "Title2")),
local = newIcal(with(TITLE, "Title")),
)
.let {
assertEquals("Title3", it.title)
}
@Test
fun remoteRemovesTitle() =
newTask(with(TaskMaker.TITLE, "Title"))
.applyRemote(
remote = newIcal(with(TITLE, null as String?)),
local = newIcal(with(TITLE, "Title")),
)
.let {
assertNull(it.title)
}
@Test
fun localRemovesTitle() =
newTask(with(TaskMaker.TITLE, null as String?))
.applyRemote(
remote = newIcal(with(TITLE, "Title")),
local = newIcal(with(TITLE, "Title"))
)
.let {
assertNull(it.title)
}
@Test
fun applyNewDescription() =
newTask()
.applyRemote(
remote = newIcal(with(DESCRIPTION, "Description")),
local = null
)
.let {
assertEquals("Description", it.notes)
}
@Test
fun localBeatsRemoteDescription() =
newTask(with(TaskMaker.DESCRIPTION, "Description3"))
.applyRemote(
remote = newIcal(with(DESCRIPTION, "Description2")),
local = newIcal(with(DESCRIPTION, "Description"))
)
.let {
assertEquals("Description3", it.notes)
}
@Test
fun remoteUpdatesDescription() {
newTask(with(TaskMaker.DESCRIPTION, "Description"))
.applyRemote(
remote = newIcal(with(DESCRIPTION, "Description2")),
local = newIcal(with(DESCRIPTION, "Description"))
)
.let {
assertEquals("Description2", it.notes)
}
}
@Test
fun localRemovedDescription() =
newTask(with(TaskMaker.DESCRIPTION, null as String?))
.applyRemote(
remote = newIcal(with(DESCRIPTION, "Description")),
local = newIcal(with(DESCRIPTION, "Description"))
)
.let {
assertNull(it.notes)
}
@Test
fun remoteRemovedDescription() =
newTask(with(TaskMaker.DESCRIPTION, "Description"))
.applyRemote(
remote = newIcal(with(DESCRIPTION, null as String?)),
local = newIcal(with(DESCRIPTION, "Description"))
)
.let {
assertNull(it.notes)
}
@Test
fun applyPriorityNewTask() =
newTask(with(TaskMaker.PRIORITY, HIGH))
.applyRemote(
remote = newIcal(with(PRIORITY, 5)),
local = null
)
.let {
assertEquals(MEDIUM, it.priority)
}
@Test
fun localUpdatedPriority() =
newTask(with(TaskMaker.PRIORITY, LOW))
.applyRemote(
remote = newIcal(with(PRIORITY, 5)),
local = newIcal(with(PRIORITY, 5))
)
.let {
assertEquals(LOW, it.priority)
}
@Test
fun remoteUpdatedPriority() =
newTask(with(TaskMaker.PRIORITY, MEDIUM))
.applyRemote(
remote = newIcal(with(PRIORITY, 1)),
local = newIcal(with(PRIORITY, 5))
)
.let {
assertEquals(HIGH, it.priority)
}
@Test
fun localBeatsRemotePriority() =
newTask(with(TaskMaker.PRIORITY, HIGH))
.applyRemote(
remote = newIcal(with(PRIORITY, 1)),
local = newIcal(with(PRIORITY, 5))
)
.let {
assertEquals(HIGH, it.priority)
}
@Test
fun dueDateNewTask() {
val due = newDateTime()
newTask()
.applyRemote(
remote = newIcal(with(DUE_DATE, due)),
local = null
)
.let {
assertEquals(due.allDay(), it.dueDate)
}
}
@Test
fun remoteAddsDueDate() {
val due = newDateTime()
newTask()
.applyRemote(
remote = newIcal(with(DUE_DATE, due)),
local = newIcal()
)
.let {
assertEquals(due.allDay(), it.dueDate)
}
}
@Test
fun remoteUpdatesDueDate() {
val due = newDateTime()
newTask(with(TaskMaker.DUE_DATE, due))
.applyRemote(
remote = newIcal(with(DUE_DATE, due.plusDays(1))),
local = newIcal(with(DUE_DATE, due))
)
.let {
assertEquals(due.plusDays(1).allDay(), it.dueDate)
}
}
@Test
fun remoteRemovesDueDate() {
val due = newDateTime()
newTask(with(TaskMaker.DUE_DATE, due))
.applyRemote(
remote = newIcal(),
local = newIcal(with(DUE_DATE, due))
)
.let {
assertEquals(0, it.dueDate)
}
}
@Test
fun localRemovesDueDate() {
val due = newDateTime()
newTask()
.applyRemote(
remote = newIcal(with(DUE_DATE, due)),
local = newIcal(with(DUE_DATE, due))
)
.let {
assertEquals(0, it.dueDate)
}
}
@Test
fun localBeatsRemoteDueDate() {
val due = newDateTime()
newTask(with(TaskMaker.DUE_DATE, due.plusDays(2)))
.applyRemote(
remote = newIcal(with(DUE_DATE, due.plusDays(1))),
local = newIcal(with(DUE_DATE, due))
)
.let {
assertEquals(due.plusDays(2).allDay(), it.dueDate)
}
}
@Test
fun startDateNewTask() {
val start = newDateTime()
newTask()
.applyRemote(
remote = newIcal(with(START_DATE, start)),
local = null
)
.let {
assertEquals(start.startOfDay().millis, it.hideUntil)
}
}
@Test
fun remoteAddsStartDate() {
val start = newDateTime()
newTask()
.applyRemote(
remote = newIcal(with(START_DATE, start)),
local = newIcal()
)
.let {
assertEquals(start.startOfDay().millis, it.hideUntil)
}
}
@Test
fun remoteUpdatesStartDate() {
val start = newDateTime()
newTask(with(TaskMaker.START_DATE, start))
.applyRemote(
remote = newIcal(with(START_DATE, start.plusDays(1))),
local = newIcal(with(START_DATE, start))
)
.let {
assertEquals(start.plusDays(1).startOfDay().millis, it.hideUntil)
}
}
@Test
fun remoteRemovesStartDate() {
val start = newDateTime()
newTask(with(TaskMaker.START_DATE, start))
.applyRemote(
remote = newIcal(),
local = newIcal(with(START_DATE, start))
)
.let {
assertEquals(0, it.hideUntil)
}
}
@Test
fun localRemovesStartDate() {
val start = newDateTime()
newTask()
.applyRemote(
remote = newIcal(with(START_DATE, start)),
local = newIcal(with(START_DATE, start))
)
.let {
assertEquals(0, it.hideUntil)
}
}
@Test
fun localBeatsRemoteStartDate() {
val start = newDateTime()
newTask(with(TaskMaker.START_DATE, start.plusDays(2)))
.applyRemote(
remote = newIcal(with(START_DATE, start.plusDays(1))),
local = newIcal(with(START_DATE, start))
)
.let {
assertEquals(start.plusDays(2).startOfDay().millis, it.hideUntil)
}
}
@Test
fun remoteAddsCreationDate() {
val created = newDateTime()
newTask(with(CREATION_TIME, created.minusMinutes(1)))
.applyRemote(
remote = newIcal(with(CREATED_AT, created.toUTC())),
local = null
)
.let {
assertEquals(created.millis, it.creationDate)
}
}
@Test
fun remoteSetsRecurrence() =
newTask()
.applyRemote(
remote = newIcal(with(RRULE, "FREQ=DAILY")),
local = null
)
.let {
assertEquals("FREQ=DAILY", it.recurrence)
}
@Test
fun remoteUpdatesRecurrence() =
newTask(with(TaskMaker.RECUR, "FREQ=DAILY"))
.applyRemote(
remote = newIcal(with(RRULE, "FREQ=MONTHLY")),
local = newIcal(with(RRULE, "FREQ=DAILY"))
)
.let {
assertEquals("FREQ=MONTHLY", it.recurrence)
}
@Test
fun remoteRemovesRecurrence() =
newTask(with(TaskMaker.RECUR, "FREQ=DAILY"))
.applyRemote(
remote = newIcal(),
local = newIcal(with(RRULE, "FREQ=DAILY"))
)
.let {
assertNull(it.recurrence)
}
@Test
fun localRemovesRecurrence() =
newTask()
.applyRemote(
remote = newIcal(with(RRULE, "FREQ=DAILY")),
local = newIcal(with(RRULE, "FREQ=DAILY"))
)
.let {
assertNull(it.recurrence)
}
@Test
fun localBeatsRemoteRecurrence() =
newTask(with(TaskMaker.RECUR, "FREQ=WEEKLY"))
.applyRemote(
remote = newIcal(with(RRULE, "FREQ=MONTHLY")),
local = newIcal(with(RRULE, "FREQ=DAILY"))
)
.let {
assertEquals("FREQ=WEEKLY", it.recurrence)
}
@Test
fun remoteSetsCompletedStatus() =
newTask()
.applyRemote(
remote = newIcal(with(STATUS, Status.VTODO_COMPLETED)),
local = null
)
.let {
assertTrue(it.isCompleted)
}
@Test
fun remoteUpdatesCompletedStatus() =
newTask()
.applyRemote(
remote = newIcal(with(STATUS, Status.VTODO_COMPLETED)),
local = newIcal(with(STATUS, Status.VTODO_IN_PROCESS))
)
.let {
assertTrue(it.isCompleted)
}
@Test
fun remoteRemovesCompletedStatus() {
val now = newDateTime()
newTask(with(COMPLETION_TIME, now))
.applyRemote(
remote = newIcal(),
local = newIcal(
with(STATUS, Status.VTODO_COMPLETED),
with(COMPLETED_AT, now)
)
)
.let {
assertFalse(it.isCompleted)
}
}
@Test
fun remoteSetsCompletedAt() {
val now = newDateTime()
newTask()
.applyRemote(
remote = newIcal(with(COMPLETED_AT, now.toUTC())),
local = null
)
.let {
assertEquals(now.startOfSecond().millis, it.completionDate)
}
}
@Test
fun remoteUpdatesCompletedAt() {
val now = newDateTime()
newTask(with(COMPLETION_TIME, now))
.applyRemote(
remote = newIcal(with(COMPLETED_AT, now.plusMinutes(5).toUTC())),
local = newIcal(
with(COMPLETED_AT, now.toUTC()),
with(STATUS, Status.VTODO_COMPLETED)
)
)
.let {
assertEquals(now.plusMinutes(5).startOfSecond().millis, it.completionDate)
}
}
@Test
fun remoteRemovesCompletedAt() {
val now = newDateTime()
newTask(with(COMPLETION_TIME, now))
.applyRemote(
remote = newIcal(),
local = newIcal(
with(COMPLETED_AT, now.toUTC()),
with(STATUS, Status.VTODO_COMPLETED)
)
)
.let {
assertFalse(it.isCompleted)
}
}
@Test
fun localRemovesCompletedAt() {
val now = newDateTime()
newTask()
.applyRemote(
remote = newIcal(with(COMPLETED_AT, now.toUTC())),
local = newIcal(
with(COMPLETED_AT, now.toUTC()),
with(STATUS, Status.VTODO_COMPLETED)
)
)
.let {
assertFalse(it.isCompleted)
}
}
@Test
fun localBeatsRemoteCompletedAt() {
val now = newDateTime()
newTask(with(COMPLETION_TIME, now.plusMinutes(2)))
.applyRemote(
remote = newIcal(with(COMPLETED_AT, now.plusMinutes(1).toUTC())),
local = newIcal(
with(COMPLETED_AT, now.toUTC()),
with(STATUS, Status.VTODO_COMPLETED)
)
)
.let {
assertEquals(now.plusMinutes(2).millis, it.completionDate)
}
}
@Test
fun remoteSetsCollapsed() {
newTask()
.applyRemote(
remote = newIcal(with(COLLAPSED, true)),
local = null
)
.let {
assertTrue(it.isCollapsed)
}
}
@Test
fun remoteRemovesCollapsed() {
newTask(with(TaskMaker.COLLAPSED, true))
.applyRemote(
remote = newIcal(),
local = newIcal(with(COLLAPSED, true))
)
.let {
assertFalse(it.isCollapsed)
}
}
@Test
fun localBeatsRemoteCollapsed() {
newTask(with(TaskMaker.COLLAPSED, true))
.applyRemote(
remote = newIcal(with(COLLAPSED, false)),
local = newIcal(with(COLLAPSED, false))
)
.let {
assertTrue(it.isCollapsed)
}
}
@Test
fun remoteSetsOrder() =
newCaldavTask()
.applyRemote(
remote = newIcal(with(ORDER, 1234)),
local = null
)
.let {
assertEquals(1234L, it.order)
}
@Test
fun remoteRemovesOrder() =
newCaldavTask(with(REMOTE_ORDER, 1234))
.applyRemote(
remote = newIcal(),
local = newIcal(with(ORDER, 1234))
)
.let {
assertNull(it.order)
}
@Test
fun localRemovesOrder() =
newCaldavTask()
.applyRemote(
remote = newIcal(with(ORDER, 1234)),
local = newIcal(with(ORDER, 1234))
)
.let {
assertNull(it.order)
}
@Test
fun localBeatsRemoteOrder() =
newCaldavTask(with(REMOTE_ORDER, 789))
.applyRemote(
remote = newIcal(with(ORDER, 456)),
local = newIcal(with(ORDER, 123))
)
.let {
assertEquals(789L, it.order)
}
@Test
fun remoteSetsParent() =
newCaldavTask()
.applyRemote(
remote = newIcal(with(PARENT, "1234")),
local = null
)
.let {
assertEquals("1234", it.remoteParent)
}
@Test
fun remoteRemovesParent() =
newCaldavTask(with(REMOTE_PARENT, "1234"))
.applyRemote(
remote = newIcal(),
local = newIcal(with(PARENT, "1234"))
)
.let {
assertNull(it.remoteParent)
}
@Test
fun localRemovesParent() =
newCaldavTask()
.applyRemote(
remote = newIcal(with(PARENT, "1234")),
local = newIcal(with(PARENT, "1234"))
)
.let {
assertNull(it.remoteParent)
}
@Test
fun localBeatsRemoteParent() =
newCaldavTask(with(REMOTE_PARENT, "789"))
.applyRemote(
remote = newIcal(with(PARENT, "456")),
local = newIcal(with(PARENT, "123"))
)
.let {
assertEquals("789", it.remoteParent)
}
companion object {
private fun DateTime.allDay() =
createDueDate(URGENCY_SPECIFIC_DAY, millis)
}
}
Loading…
Cancel
Save