Merge remote changes before pushing local changes

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

@ -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.iCalendar.Companion.applyRemote
import org.tasks.caldav.applyRemote
import org.tasks.caldav.iCalendar.Companion.reminders
import org.tasks.data.Alarm
import org.tasks.data.CaldavTask
@ -42,7 +42,7 @@ object TestUtilities {
fun vtodo(path: String): Task {
val task = Task()
task.applyRemote(fromResource(path))
task.applyRemote(fromResource(path), null)
return task
}
@ -53,7 +53,7 @@ object TestUtilities {
val task = Task()
val vtodo = readFile(path)
val remote = fromString(vtodo)
task.applyRemote(remote)
task.applyRemote(remote, null)
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.PropertyValue
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 org.tasks.Strings
import org.tasks.date.DateTimeUtils
@ -16,6 +17,7 @@ object TaskMaker {
val ID: Property<Task, Long> = newProperty()
val DUE_DATE: 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 HIDE_TYPE: Property<Task, Int> = newProperty()
val REMINDERS: Property<Task, Int> = newProperty()
@ -30,6 +32,7 @@ object TaskMaker {
val PARENT: Property<Task, Long> = newProperty()
val UUID: Property<Task, String> = newProperty()
val COLLAPSED: Property<Task, Boolean> = newProperty()
val DESCRIPTION: Property<Task, String?> = newProperty()
private val instantiator = Instantiator { lookup: PropertyLookup<Task> ->
val task = Task()
@ -61,6 +64,9 @@ object TaskMaker {
if (deletedTime != null) {
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)
if (hideType >= 0) {
task.hideUntil = task.createHideUntil(hideType, 0)
@ -76,6 +82,7 @@ object TaskMaker {
lookup.valueOf(RECUR, null as String?)?.let {
task.setRecurrence(it, lookup.valueOf(AFTER_COMPLETE, false))
}
task.notes = lookup.valueOf(DESCRIPTION, null as String?)
task.isCollapsed = lookup.valueOf(COLLAPSED, false)
task.uuid = lookup.valueOf(UUID, NO_UUID)
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
.principals(account, calendar)
.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, "")
}
@ -218,20 +221,17 @@ class CaldavSynchronizer @Inject constructor(
}
}
private suspend fun sync(
private suspend fun fetchChanges(
caldavCalendar: CaldavCalendar,
resource: Response,
httpClient: OkHttpClient) {
Timber.d("sync(%s)", caldavCalendar)
val httpUrl = resource.href
if (caldavCalendar.access != ACCESS_READ_ONLY) {
pushLocalChanges(caldavCalendar, httpClient, httpUrl)
}
val remoteCtag = resource.ctag
if (caldavCalendar.ctag?.equals(remoteCtag) == true) {
Timber.d("%s up to date", caldavCalendar.name)
return
}
Timber.d("updating $caldavCalendar")
val davCalendar = DavCalendar(httpClient, httpUrl)
val members = ArrayList<Response>()
davCalendar.calendarQuery("VTODO", null, null) { response, relation ->

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

@ -3,7 +3,6 @@ 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.alarms.AlarmService
import com.todoroo.astrid.dao.TaskDao
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.component.VAlarm
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.Completed
import net.fortuna.ical4j.model.property.DateProperty
@ -53,7 +51,6 @@ import org.tasks.location.GeofenceApi
import org.tasks.preferences.Preferences
import org.tasks.repeats.RecurrenceUtils.newRRule
import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.time.DateTime.UTC
import org.tasks.time.DateTimeUtils.startOfDay
import org.tasks.time.DateTimeUtils.startOfMinute
import org.tasks.time.DateTimeUtils.toDate
@ -139,14 +136,10 @@ class iCalendar @Inject constructor(
remoteModel = Task()
}
toVtodo(caldavTask, task, remoteModel)
val os = ByteArrayOutputStream()
remoteModel.write(os)
return os.toByteArray()
return toVtodo(caldavTask, task, remoteModel)
}
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)
val categories = remoteModel.categories
categories.clear()
@ -167,6 +160,9 @@ class iCalendar @Inject constructor(
val alarms = alarmDao.getAlarms(task.id)
remoteModel.snooze = alarms.find { it.type == TYPE_SNOOZE }?.time
remoteModel.alarms.addAll(alarms.toVAlarms())
val os = ByteArrayOutputStream()
remoteModel.write(os)
return os.toByteArray()
}
suspend fun fromVtodo(
@ -175,29 +171,47 @@ class iCalendar @Inject constructor(
remote: Task,
vtodo: String?,
obj: String? = null,
eTag: String? = null) {
eTag: String? = null
) {
val task = existing?.task?.let { taskDao.fetch(it) }
?: taskCreator.createWithValues("").apply {
taskDao.createNew(this)
existing?.task = id
}
val caldavTask = existing ?: CaldavTask(task.id, calendar.uuid, remote.uid, obj)
task.applyRemote(remote)
setPlace(task.id, remote.geoPosition)
tagDao.applyTags(task, tagDataDao, getTags(remote.categories))
val randomReminders = alarmDao.getAlarms(task.id).filter { it.type == TYPE_RANDOM }
alarmService.synchronizeAlarms(
caldavTask.task,
remote.reminders.plus(randomReminders).toMutableSet()
)
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)
}
val tags = tagDataDao.getTagDataForTask(task.id)
val localTags = getTags(local?.categories ?: emptyList())
if (tags.toSet() == localTags.toSet()) {
tagDao.applyTags(task, tagDataDao, getTags(remote.categories))
}
val alarms = alarmDao.getAlarms(task.id)
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.suppressRefresh()
taskDao.save(task)
caldavTask.vtodo = vtodo
caldavTask.etag = eTag
caldavTask.lastSync = task.modificationDate
caldavTask.remoteParent = remote.parent
caldavTask.order = remote.order
if (!dirty) {
caldavTask.lastSync = task.modificationDate
}
if (caldavTask.id == com.todoroo.astrid.data.Task.NO_ID) {
caldavTask.id = caldavDao.insert(caldavTask)
Timber.d("NEW %s", caldavTask)
@ -227,26 +241,32 @@ class iCalendar @Inject constructor(
private val IS_MOZ_LASTACK = { x: Property? -> x?.name.equals(MOZ_LASTACK, true) }
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
is DateTime -> com.todoroo.astrid.data.Task.createDueDate(
URGENCY_SPECIFIC_DAY_TIME,
getLocal(this)
URGENCY_SPECIFIC_DAY_TIME,
getLocal(this)
)
else -> com.todoroo.astrid.data.Task.createDueDate(
URGENCY_SPECIFIC_DAY,
getLocal(this)
URGENCY_SPECIFIC_DAY,
getLocal(this)
)
}
}
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
is DateTime -> task.createHideUntil(HIDE_UNTIL_SPECIFIC_DAY_TIME, getLocal(this))
else -> task.createHideUntil(HIDE_UNTIL_SPECIFIC_DAY, getLocal(this))
}
}
internal fun getLocal(property: DateProperty): Long =
org.tasks.time.DateTime.from(property.date)?.toLocal()?.millis ?: 0
@ -334,35 +354,6 @@ class iCalendar @Inject constructor(
?: 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) {
createdAt = newDateTime(task.creationDate).toUTC().millis
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 com.etebase.client.Collection
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.service.TaskDeleter
import dagger.hilt.android.qualifiers.ApplicationContext
@ -22,7 +26,6 @@ import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.time.DateTimeUtils.currentTimeMillis
import timber.log.Timber
import java.util.*
import javax.inject.Inject
class EtebaseSynchronizer @Inject constructor(
@ -92,7 +95,8 @@ class EtebaseSynchronizer @Inject constructor(
caldavDao.update(calendar)
localBroadcastManager.broadcastRefreshList()
}
sync(client, calendar, collection)
fetchChanges(client, calendar, collection)
pushLocalChanges(client, calendar, collection)
}
setError(account, "")
}
@ -109,18 +113,16 @@ class EtebaseSynchronizer @Inject constructor(
}
}
private suspend fun sync(
private suspend fun fetchChanges(
client: EtebaseClient,
caldavCalendar: CaldavCalendar,
collection: Collection
) {
Timber.d("sync(%s)", caldavCalendar)
pushLocalChanges(client, caldavCalendar, collection)
val localCtag = caldavCalendar.ctag
if (localCtag != null && localCtag == collection.stoken) {
if (caldavCalendar.ctag?.equals(collection.stoken) == true) {
Timber.d("${caldavCalendar.name} up to date")
return
}
Timber.d("updating $caldavCalendar")
client.fetchItems(collection, caldavCalendar) { (stoken, items) ->
applyEntries(caldavCalendar, items, stoken)
client.updateCache(collection, items)

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

@ -134,6 +134,12 @@ public class DateTime {
return new DateTime(calendar);
}
public DateTime startOfSecond() {
Calendar calendar = getCalendar();
calendar.set(Calendar.MILLISECOND, 0);
return new DateTime(calendar);
}
public DateTime endOfMinute() {
Calendar calendar = getCalendar();
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.startOfSecond(): Long = if (this > 0) toDateTime().startOfSecond().millis else 0
fun Long.millisOfDay(): Int = if (this > 0) toDateTime().millisOfDay else 0
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