Store icalendar data on disk

pull/1793/head
Alex Baker 2 years ago
parent 433ae41200
commit 1154ba4be4

File diff suppressed because it is too large Load Diff

@ -16,6 +16,8 @@ import org.tasks.data.GoogleTaskDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.jobs.WorkManager
import org.tasks.makers.CaldavCalendarMaker.UUID
import org.tasks.makers.CaldavCalendarMaker.newCaldavCalendar
import org.tasks.makers.CaldavTaskMaker
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
@ -79,6 +81,7 @@ class TaskMoverTest : InjectingTestCase() {
@Test
fun moveBetweenCaldavList() = runBlocking {
createTasks(1)
caldavDao.insert(newCaldavCalendar(with(UUID, "1")))
caldavDao.insert(newCaldavTask(with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1")))
moveToCaldavList("2", 1)
assertEquals("2", caldavDao.getTask(1)!!.calendar)
@ -87,6 +90,7 @@ class TaskMoverTest : InjectingTestCase() {
@Test
fun deleteCaldavTaskAfterMove() = runBlocking {
createTasks(1)
caldavDao.insert(newCaldavCalendar(with(UUID, "1")))
caldavDao.insert(newCaldavTask(with(CaldavTaskMaker.TASK, 1L), with(CALENDAR, "1")))
moveToCaldavList("2", 1)
val deleted = caldavDao.getMoved("1")
@ -100,6 +104,7 @@ class TaskMoverTest : InjectingTestCase() {
createTasks(1)
createSubtask(2, 1)
createSubtask(3, 2)
caldavDao.insert(newCaldavCalendar(with(UUID, "1")))
caldavDao.insert(
listOf(
newCaldavTask(
@ -196,6 +201,7 @@ class TaskMoverTest : InjectingTestCase() {
fun moveCaldavChildWithoutParent() = runBlocking {
createTasks(1)
createSubtask(2, 1)
caldavDao.insert(newCaldavCalendar(with(UUID, "1")))
caldavDao.insert(
listOf(
newCaldavTask(
@ -266,6 +272,7 @@ class TaskMoverTest : InjectingTestCase() {
fun dontDuplicateWhenParentAndChildCaldavMoved() = runBlocking {
createTasks(1)
createSubtask(2, 1)
caldavDao.insert(newCaldavCalendar(with(UUID, "1")))
caldavDao.insert(
listOf(
newCaldavTask(

@ -7,17 +7,20 @@ import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.tasks.SuspendFreeze.Companion.freezeAt
import org.tasks.TestUtilities.assertEquals
import org.tasks.caldav.VtodoCache
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.data.TaskDao
import org.tasks.injection.InjectingTestCase
import org.tasks.injection.ProductionModule
import org.tasks.makers.CaldavCalendarMaker.newCaldavCalendar
import org.tasks.makers.CaldavTaskMaker.CALENDAR
import org.tasks.makers.CaldavTaskMaker.REMOTE_ID
import org.tasks.makers.CaldavTaskMaker.TASK
import org.tasks.makers.CaldavTaskMaker.VTODO
import org.tasks.makers.CaldavTaskMaker.newCaldavTask
import org.tasks.makers.TaskMaker.DUE_DATE
import org.tasks.makers.TaskMaker.HIDE_TYPE
@ -34,11 +37,26 @@ class Upgrade_11_3_Test : InjectingTestCase() {
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var openTaskDao: TestOpenTaskDao
@Inject lateinit var upgrader: Upgrade_11_3
@Inject lateinit var vtodoCache: VtodoCache
private lateinit var calendar: CaldavCalendar
@Before
override fun setUp() {
super.setUp()
calendar = newCaldavCalendar()
runBlocking {
caldavDao.insert(calendar)
}
}
@Test
fun applyRemoteiCalendarStartDate() = runBlocking {
val taskId = taskDao.insert(newTask())
caldavDao.insert(newCaldavTask(with(TASK, taskId), with(VTODO, VTODO_WITH_START_DATE)))
val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid))
caldavDao.insert(caldavTask)
vtodoCache.putVtodo(calendar, caldavTask, VTODO_WITH_START_DATE)
upgrader.applyiCalendarStartDates()
assertEquals(DateTime(2021, 1, 21), taskDao.fetch(taskId)?.hideUntil)
@ -50,7 +68,10 @@ class Upgrade_11_3_Test : InjectingTestCase() {
with(DUE_DATE, DateTime(2021, 1, 20)),
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE)
))
caldavDao.insert(newCaldavTask(with(TASK, taskId), with(VTODO, VTODO_WITH_START_DATE)))
val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid))
caldavDao.insert(caldavTask)
vtodoCache.putVtodo(calendar, caldavTask, VTODO_WITH_START_DATE)
upgrader.applyiCalendarStartDates()
assertEquals(DateTime(2021, 1, 20), taskDao.fetch(taskId)?.hideUntil)
@ -64,7 +85,9 @@ class Upgrade_11_3_Test : InjectingTestCase() {
with(HIDE_TYPE, Task.HIDE_UNTIL_DUE),
with(MODIFICATION_TIME, DateTime(2021, 1, 21, 9, 50, 4, 348))
))
caldavDao.insert(newCaldavTask(with(TASK, taskId), with(VTODO, VTODO_WITH_START_DATE)))
val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid))
caldavDao.insert(caldavTask)
vtodoCache.putVtodo(calendar, caldavTask, VTODO_WITH_START_DATE)
freezeAt(upgradeTime) {
upgrader.applyiCalendarStartDates()
@ -77,7 +100,9 @@ class Upgrade_11_3_Test : InjectingTestCase() {
fun dontTouchWhenNoiCalendarStartDate() = runBlocking {
val modificationTime = DateTime(2021, 1, 21, 9, 50, 4, 348)
val taskId = taskDao.insert(newTask(with(MODIFICATION_TIME, modificationTime)))
caldavDao.insert(newCaldavTask(with(TASK, taskId), with(VTODO, VTODO_NO_START_DATE)))
val caldavTask = newCaldavTask(with(TASK, taskId), with(CALENDAR, calendar.uuid))
caldavDao.insert(caldavTask)
vtodoCache.putVtodo(calendar, caldavTask, VTODO_NO_START_DATE)
upgrader.applyiCalendarStartDates()

@ -54,7 +54,7 @@ object TestUtilities {
val vtodo = readFile(path)
val remote = fromString(vtodo)
task.applyRemote(remote, null)
return Triple(task, CaldavTask().apply { this.vtodo = vtodo }, remote)
return Triple(task, CaldavTask(), remote)
}
private fun fromResource(path: String): at.bitfire.ical4android.Task =

@ -0,0 +1,24 @@
package org.tasks.makers
import com.natpryce.makeiteasy.Instantiator
import com.natpryce.makeiteasy.Property
import com.natpryce.makeiteasy.Property.newProperty
import com.natpryce.makeiteasy.PropertyValue
import org.tasks.data.CaldavCalendar
import org.tasks.makers.Maker.make
object CaldavCalendarMaker {
val ACCOUNT: Property<CaldavCalendar, String> = newProperty()
val UUID: Property<CaldavCalendar, String> = newProperty()
private val instantiator = Instantiator<CaldavCalendar> { lookup ->
val calendar = CaldavCalendar()
calendar.account = lookup.valueOf(ACCOUNT, "account")
calendar.uuid = lookup.valueOf(UUID, "uuid")
calendar
}
fun newCaldavCalendar(vararg properties: PropertyValue<in CaldavCalendar?, *>): CaldavCalendar {
return make(instantiator, *properties)
}
}

@ -13,7 +13,6 @@ object CaldavTaskMaker {
val REMOTE_ID: Property<CaldavTask, String?> = newProperty()
val REMOTE_PARENT: Property<CaldavTask, String?> = newProperty()
val REMOTE_ORDER: Property<CaldavTask, Long?> = newProperty()
val VTODO: Property<CaldavTask, String?> = newProperty()
val ETAG: Property<CaldavTask, String?> = newProperty()
val OBJECT: Property<CaldavTask, String?> = newProperty()
@ -21,7 +20,6 @@ object CaldavTaskMaker {
val task = CaldavTask(it.valueOf(TASK, 1L), it.valueOf(CALENDAR, "calendar"))
task.remoteId = it.valueOf(REMOTE_ID, task.remoteId)
task.remoteParent = it.valueOf(REMOTE_PARENT, null as String?)
task.vtodo = it.valueOf(VTODO, null as String?)
task.order = it.valueOf(REMOTE_ORDER, null as Long?)
task.etag = it.valueOf(ETAG, null as String?)
task.`object` = it.valueOf(OBJECT, task.remoteId?.let { id -> "$id.ics" })

@ -61,7 +61,7 @@ import org.tasks.notifications.NotificationDao
Principal::class,
PrincipalAccess::class
],
version = 81
version = 82
)
abstract class Database : RoomDatabase() {
abstract fun notificationDao(): NotificationDao

@ -5,13 +5,20 @@ import com.todoroo.astrid.data.Task
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.runBlocking
import org.tasks.LocalBroadcastManager
import org.tasks.data.*
import org.tasks.caldav.VtodoCache
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.DeletionDao
import org.tasks.data.GoogleTaskAccount
import org.tasks.data.GoogleTaskDao
import org.tasks.data.GoogleTaskList
import org.tasks.data.TaskContainer
import org.tasks.data.TaskDao
import org.tasks.db.QueryUtils
import org.tasks.db.SuspendDbUtils.chunkedMap
import org.tasks.jobs.WorkManager
import org.tasks.preferences.Preferences
import org.tasks.sync.SyncAdapters
import java.util.*
import javax.inject.Inject
class TaskDeleter @Inject constructor(
@ -21,7 +28,9 @@ class TaskDeleter @Inject constructor(
private val localBroadcastManager: LocalBroadcastManager,
private val googleTaskDao: GoogleTaskDao,
private val preferences: Preferences,
private val syncAdapters: SyncAdapters) {
private val syncAdapters: SyncAdapters,
private val vtodoCache: VtodoCache,
) {
suspend fun markDeleted(item: Task) = markDeleted(persistentListOf(item.id))
@ -73,12 +82,14 @@ class TaskDeleter @Inject constructor(
}
suspend fun delete(list: CaldavCalendar) {
vtodoCache.delete(list)
val tasks = deletionDao.delete(list)
delete(tasks)
localBroadcastManager.broadcastRefreshList()
}
suspend fun delete(list: CaldavAccount) {
vtodoCache.delete(list)
val tasks = deletionDao.delete(list)
delete(tasks)
localBroadcastManager.broadcastRefreshList()

@ -9,11 +9,16 @@ import com.todoroo.astrid.data.Task
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.BuildConfig
import org.tasks.LocalBroadcastManager
import org.tasks.data.*
import org.tasks.caldav.VtodoCache
import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask
import org.tasks.data.GoogleTask
import org.tasks.data.GoogleTaskDao
import org.tasks.data.GoogleTaskListDao
import org.tasks.data.TaskDao
import org.tasks.db.DbUtils.dbchunk
import org.tasks.preferences.Preferences
import org.tasks.sync.SyncAdapters
import java.util.*
import javax.inject.Inject
class TaskMover @Inject constructor(
@ -24,7 +29,9 @@ class TaskMover @Inject constructor(
private val googleTaskListDao: GoogleTaskListDao,
private val preferences: Preferences,
private val localBroadcastManager: LocalBroadcastManager,
private val syncAdapters: SyncAdapters) {
private val syncAdapters: SyncAdapters,
private val vtodoCache: VtodoCache,
) {
suspend fun getSingleFilter(tasks: List<Long>): Filter? {
val caldavCalendars = caldavDao.getCalendars(tasks)
@ -129,15 +136,16 @@ class TaskMover @Inject constructor(
caldavDao.markDeleted(toDelete, DateUtilities.now())
when (selected) {
is CaldavFilter -> {
val from = caldavDao.getCalendar(caldavTask.calendar!!)
val id1 = caldavTask.task
val listId = selected.uuid
val newParent = CaldavTask(id1, listId, caldavTask.remoteId, caldavTask.`object`)
newParent.vtodo = caldavTask.vtodo
vtodoCache.move(from!!, selected.calendar, caldavTask)
caldavDao.insert(task, newParent, preferences.addTasksToTop())
children.takeIf { it.isNotEmpty() }
?.map {
val newChild = CaldavTask(it.task, listId, it.remoteId, it.`object`)
newChild.vtodo = it.vtodo
vtodoCache.move(from, selected.calendar, it)
newChild.remoteParent = it.remoteParent
newChild
}

@ -2,6 +2,7 @@
package com.todoroo.astrid.service
import org.tasks.caldav.VtodoCache
import org.tasks.caldav.iCalendar
import org.tasks.caldav.iCalendar.Companion.apply
import org.tasks.data.OpenTaskDao
@ -12,16 +13,21 @@ import javax.inject.Inject
class Upgrade_11_3 @Inject constructor(
private val upgraderDao: UpgraderDao,
private val openTaskDao: OpenTaskDao,
private val taskDao: TaskDao
private val taskDao: TaskDao,
private val vtodoCache: VtodoCache,
) {
internal suspend fun applyiCalendarStartDates() {
val (hasStartDate, noStartDate) =
upgraderDao.tasksWithVtodos().partition { it.startDate > 0 }
for (task in noStartDate) {
task.vtodo?.let { iCalendar.fromVtodo(it) }?.dtStart?.let {
it.apply(task.task)
upgraderDao.setStartDate(task.id, task.startDate)
}
vtodoCache
.getVtodo(task.caldavTask)
?.let { iCalendar.fromVtodo(it) }
?.dtStart
?.let {
it.apply(task.task)
upgraderDao.setStartDate(task.id, task.startDate)
}
}
hasStartDate
.map { it.id }

@ -2,9 +2,11 @@
package com.todoroo.astrid.service
import org.tasks.caldav.VtodoCache
import org.tasks.caldav.iCalendar
import org.tasks.caldav.iCalendar.Companion.reminders
import org.tasks.data.AlarmDao
import org.tasks.data.CaldavTaskContainer
import org.tasks.data.TaskDao
import org.tasks.data.UpgraderDao
import javax.inject.Inject
@ -13,15 +15,17 @@ class Upgrade_12_4 @Inject constructor(
private val alarmDao: AlarmDao,
private val taskDao: TaskDao,
private val upgraderDao: UpgraderDao,
private val vtodoCache: VtodoCache,
) {
internal suspend fun syncExistingAlarms() {
val existingAlarms = alarmDao.getActiveAlarms()
upgraderDao.tasksWithVtodos().forEach { caldav ->
val remoteTask = caldav.vtodo?.let(iCalendar::fromVtodo) ?: return@forEach
upgraderDao.tasksWithVtodos().map(CaldavTaskContainer::caldavTask).forEach { task ->
val remoteTask =
vtodoCache.getVtodo(task)?.let(iCalendar::fromVtodo) ?: return@forEach
remoteTask
.reminders
.filter { existingAlarms.find { e -> e.task == caldav.id && e.same(it) } == null }
.onEach { it.task = caldav.id }
.filter { existingAlarms.find { e -> e.task == task.task && e.same(it) } == null }
.onEach { it.task = task.task }
.let { alarmDao.insert(it) }
}
taskDao.touch(existingAlarms.map { it.task }.toSet().toList())

@ -12,6 +12,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.runBlocking
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.VtodoCache
import org.tasks.caldav.iCalendar
import org.tasks.caldav.iCalendar.Companion.fromVtodo
import org.tasks.caldav.iCalendar.Companion.order
@ -57,6 +58,7 @@ class Upgrader @Inject constructor(
private val widgetManager: AppWidgetManager,
private val taskMover: TaskMover,
private val upgraderDao: UpgraderDao,
private val vtodoCache: VtodoCache,
private val upgrade_11_3: Lazy<Upgrade_11_3>,
private val upgrade_11_12_3: Lazy<Upgrade_11_12_3>,
private val upgrade_12_4: Lazy<Upgrade_12_4>,
@ -150,10 +152,9 @@ class Upgrader @Inject constructor(
return getAndroidColor(context, index)
}
private suspend fun applyCaldavOrder() {
for (task in upgraderDao.tasksWithVtodos().map(CaldavTaskContainer::caldavTask)) {
val remoteTask = fromVtodo(task.vtodo!!) ?: continue
val remoteTask = vtodoCache.getVtodo(task)?.let { fromVtodo(it) } ?: continue
val order: Long? = remoteTask.order
if (order != null) {
task.order = order
@ -169,7 +170,7 @@ class Upgrader @Inject constructor(
if (tasksWithLocations.contains(taskId)) {
continue
}
val remoteTask = fromVtodo(task.vtodo!!) ?: continue
val remoteTask = vtodoCache.getVtodo(task)?.let { fromVtodo(it) } ?: continue
val geo = remoteTask.geoPosition ?: continue
iCal.setPlace(taskId, geo)
}
@ -179,7 +180,7 @@ class Upgrader @Inject constructor(
private suspend fun applyCaldavSubtasks() {
val updated: MutableList<CaldavTask> = ArrayList()
for (task in upgraderDao.tasksWithVtodos().map(CaldavTaskContainer::caldavTask)) {
val remoteTask = fromVtodo(task.vtodo!!) ?: continue
val remoteTask = vtodoCache.getVtodo(task)?.let { fromVtodo(it) } ?: continue
task.remoteParent = remoteTask.parent
if (!isNullOrEmpty(task.remoteParent)) {
updated.add(task)
@ -192,10 +193,9 @@ class Upgrader @Inject constructor(
private suspend fun applyCaldavCategories() {
val tasksWithTags: List<Long> = upgraderDao.tasksWithTags()
for (container in upgraderDao.tasksWithVtodos()) {
val remoteTask = fromVtodo(container.vtodo!!)
if (remoteTask != null) {
tagDao.insert(container.task, iCal.getTags(remoteTask.categories))
}
val remoteTask =
vtodoCache.getVtodo(container.caldavTask)?.let { fromVtodo(it) } ?: continue
tagDao.insert(container.task, iCal.getTags(remoteTask.categories))
}
taskDao.touch(tasksWithTags)
}

@ -2,7 +2,21 @@ package org.tasks.backup
import com.todoroo.astrid.data.Task
import org.tasks.backup.TasksJsonImporter.LegacyLocation
import org.tasks.data.*
import org.tasks.data.Alarm
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavTask
import org.tasks.data.Filter
import org.tasks.data.Geofence
import org.tasks.data.GoogleTask
import org.tasks.data.GoogleTaskAccount
import org.tasks.data.GoogleTaskList
import org.tasks.data.Place
import org.tasks.data.Tag
import org.tasks.data.TagData
import org.tasks.data.TaskAttachment
import org.tasks.data.TaskListMetadata
import org.tasks.data.UserActivity
class BackupContainer(
val tasks: List<TaskBackup>?,
@ -28,8 +42,9 @@ class BackupContainer(
val google: List<GoogleTask>,
val comments: List<UserActivity>,
val attachments: List<TaskAttachment>?,
val caldavTasks: List<CaldavTask>?) {
val caldavTasks: List<CaldavTask>?,
val vtodo: String?,
) {
val locations: List<LegacyLocation> = emptyList()
}
}

@ -14,7 +14,19 @@ import com.todoroo.astrid.data.Task
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.backup.BackupContainer.TaskBackup
import org.tasks.data.*
import org.tasks.caldav.VtodoCache
import org.tasks.data.AlarmDao
import org.tasks.data.CaldavDao
import org.tasks.data.FilterDao
import org.tasks.data.GoogleTaskDao
import org.tasks.data.GoogleTaskListDao
import org.tasks.data.LocationDao
import org.tasks.data.TagDao
import org.tasks.data.TagDataDao
import org.tasks.data.TaskAttachmentDao
import org.tasks.data.TaskDao
import org.tasks.data.TaskListMetadataDao
import org.tasks.data.UserActivityDao
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.extensions.Context.toast
import org.tasks.files.FileHelper
@ -26,7 +38,6 @@ import java.io.IOException
import java.io.OutputStream
import java.io.OutputStreamWriter
import java.nio.charset.Charset
import java.util.*
import javax.inject.Inject
class TasksJsonExporter @Inject constructor(
@ -43,7 +54,9 @@ class TasksJsonExporter @Inject constructor(
private val taskAttachmentDao: TaskAttachmentDao,
private val caldavDao: CaldavDao,
private val workManager: WorkManager,
private val taskListMetadataDao: TaskListMetadataDao) {
private val taskListMetadataDao: TaskListMetadataDao,
private val vtodoCache: VtodoCache,
) {
private var context: Context? = null
private var exportCount = 0
@ -108,16 +121,19 @@ class TasksJsonExporter @Inject constructor(
for (task in tasks) {
setProgress(taskBackups.size, tasks.size)
val taskId = task.id
val caldavTasks = caldavDao.getTasks(taskId)
taskBackups.add(
TaskBackup(
task,
alarmDao.getAlarms(taskId),
locationDao.getGeofencesForTask(taskId),
tagDao.getTagsForTask(taskId),
googleTaskDao.getAllByTaskId(taskId),
userActivityDao.getCommentsForTask(task.uuid),
taskAttachmentDao.getAttachments(task.uuid),
caldavDao.getTasks(taskId)))
task,
alarmDao.getAlarms(taskId),
locationDao.getGeofencesForTask(taskId),
tagDao.getTagsForTask(taskId),
googleTaskDao.getAllByTaskId(taskId),
userActivityDao.getCommentsForTask(task.uuid),
taskAttachmentDao.getAttachments(task.uuid),
caldavTasks,
vtodoCache.getVtodo( caldavTasks.firstOrNull { !it.isDeleted() })
))
}
val data: MutableMap<String, Any> = HashMap()
data["version"] = BuildConfig.VERSION_CODE

@ -16,6 +16,7 @@ import com.todoroo.astrid.service.Upgrader.Companion.V6_4
import com.todoroo.astrid.service.Upgrader.Companion.getAndroidColor
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.caldav.VtodoCache
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.AlarmDao
@ -56,7 +57,9 @@ class TasksJsonImporter @Inject constructor(
private val caldavDao: CaldavDao,
private val preferences: Preferences,
private val taskMover: TaskMover,
private val taskListMetadataDao: TaskListMetadataDao) {
private val taskListMetadataDao: TaskListMetadataDao,
private val vtodoCache: VtodoCache,
) {
private val result = ImportResult()
@ -219,6 +222,12 @@ class TasksJsonImporter @Inject constructor(
caldavTask.task = taskId
caldavDao.insert(caldavTask)
}
backup.vtodo?.let {
val caldavTask =
backup.caldavTasks?.firstOrNull { t -> !t.isDeleted() } ?: return@let
val caldavCalendar = caldavDao.getCalendar(caldavTask.calendar!!) ?: return@let
vtodoCache.putVtodo(caldavCalendar, caldavTask, it)
}
result.importCount++
}
googleTaskDao.updateParents()

@ -92,6 +92,7 @@ class CaldavSynchronizer @Inject constructor(
private val provider: CaldavClientProvider,
private val iCal: iCalendar,
private val principalDao: PrincipalDao,
private val vtodoCache: VtodoCache,
) {
suspend fun sync(account: CaldavAccount) {
Thread.currentThread().contextClassLoader = context.classLoader
@ -293,11 +294,11 @@ class CaldavSynchronizer @Inject constructor(
private suspend fun pushLocalChanges(
caldavCalendar: CaldavCalendar, httpClient: OkHttpClient, httpUrl: HttpUrl) {
for (task in caldavDao.getMoved(caldavCalendar.uuid!!)) {
deleteRemoteResource(httpClient, httpUrl, task)
deleteRemoteResource(httpClient, httpUrl, caldavCalendar, task)
}
for (task in taskDao.getCaldavTasksToPush(caldavCalendar.uuid!!)) {
try {
pushTask(task, httpClient, httpUrl)
pushTask(caldavCalendar, task, httpClient, httpUrl)
} catch (e: IOException) {
Timber.e(e)
}
@ -305,7 +306,11 @@ class CaldavSynchronizer @Inject constructor(
}
private suspend fun deleteRemoteResource(
httpClient: OkHttpClient, httpUrl: HttpUrl, caldavTask: CaldavTask): Boolean {
httpClient: OkHttpClient,
httpUrl: HttpUrl,
calendar: CaldavCalendar,
caldavTask: CaldavTask
): Boolean {
try {
if (!isNullOrEmpty(caldavTask.`object`)) {
val remote = DavResource(
@ -321,20 +326,26 @@ class CaldavSynchronizer @Inject constructor(
Timber.e(e)
return false
}
vtodoCache.delete(calendar, caldavTask)
caldavDao.delete(caldavTask)
return true
}
private suspend fun pushTask(task: Task, httpClient: OkHttpClient, httpUrl: HttpUrl) {
private suspend fun pushTask(
calendar: CaldavCalendar,
task: Task,
httpClient: OkHttpClient,
httpUrl: HttpUrl
) {
Timber.d("pushing %s", task)
val caldavTask = caldavDao.getTask(task.id) ?: return
if (task.isDeleted) {
if (deleteRemoteResource(httpClient, httpUrl, caldavTask)) {
if (deleteRemoteResource(httpClient, httpUrl, calendar, caldavTask)) {
taskDeleter.delete(task)
}
return
}
val data = iCal.toVtodo(caldavTask, task)
val data = iCal.toVtodo(calendar, caldavTask, task)
val requestBody = RequestBody.create(MIME_ICALENDAR, data)
try {
val remote = DavResource(
@ -343,7 +354,7 @@ class CaldavSynchronizer @Inject constructor(
val getETag = fromResponse(it)
if (getETag != null && !isNullOrEmpty(getETag.eTag)) {
caldavTask.etag = getETag.eTag
caldavTask.vtodo = String(data)
vtodoCache.putVtodo(calendar, caldavTask, String(data))
}
}
} catch (e: HttpException) {

@ -0,0 +1,30 @@
package org.tasks.caldav
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject
class FileStorage @Inject constructor(
@ApplicationContext context: Context
) {
val root = File(context.filesDir, "vtodo")
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
fun getFile(vararg segments: String?): File? =
if (segments.none { it.isNullOrBlank() }) {
segments.fold(root) { f, p -> File(f, p) }
} else {
null
}
fun read(file: File?): String? = file?.takeIf { it.exists() }?.readText()
fun write(file: File, data: String?) {
if (data.isNullOrBlank()) {
file.delete()
} else {
file.writeText(data)
}
}
}

@ -0,0 +1,76 @@
package org.tasks.caldav
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class VtodoCache @Inject constructor(
private val caldavDao: CaldavDao,
private val fileStorage: FileStorage,
) {
fun move(from: CaldavCalendar, to: CaldavCalendar, task: CaldavTask) {
val source =
fileStorage.getFile(from.account, from.uuid, task.`object`)
if (source?.exists() != true) {
return
}
val target =
fileStorage.getFile(to.account, to.uuid)
?.apply { mkdirs() }
?.let { File(it, task.`object`!!) }
?: return
source.copyTo(target, overwrite = true)
source.delete()
}
suspend fun getVtodo(caldavTask: CaldavTask?): String? {
if (caldavTask == null) {
return null
}
val calendar = caldavDao.getCalendar(caldavTask.calendar!!) ?: return null
return getVtodo(calendar, caldavTask)
}
fun getVtodo(calendar: CaldavCalendar?, caldavTask: CaldavTask?): String? {
val file = fileStorage.getFile(
calendar?.account,
caldavTask?.calendar,
caldavTask?.`object`
)
return fileStorage.read(file)
}
fun putVtodo(calendar: CaldavCalendar, caldavTask: CaldavTask, vtodo: String?) {
val directory =
fileStorage
.getFile(calendar.account, caldavTask.calendar)
?.apply { mkdirs() }
?: return
fileStorage.write(File(directory, caldavTask.`object`!!), vtodo)
}
suspend fun delete(taskIds: List<Long>) {
val tasks = caldavDao.getTasks(taskIds).groupBy { it.calendar!! }
tasks.forEach { (c, t) ->
val calendar = caldavDao.getCalendar(c) ?: return@forEach
t.forEach { delete(calendar, it) }
}
}
fun delete(calendar: CaldavCalendar, caldavTask: CaldavTask) {
fileStorage
.getFile(calendar.account!!, caldavTask.calendar!!, caldavTask.`object`!!)
?.delete()
}
fun delete(calendar: CaldavCalendar) =
fileStorage.getFile(calendar.account!!, calendar.uuid!!)?.deleteRecursively()
fun delete(account: CaldavAccount) =
fileStorage.getFile(account.uuid!!)?.deleteRecursively()
}

@ -76,6 +76,7 @@ class iCalendar @Inject constructor(
private val caldavDao: CaldavDao,
private val alarmDao: AlarmDao,
private val alarmService: AlarmService,
private val vtodoCache: VtodoCache,
) {
suspend fun setPlace(taskId: Long, geo: Geo?) {
@ -123,11 +124,16 @@ class iCalendar @Inject constructor(
return tags
}
suspend fun toVtodo(caldavTask: CaldavTask, task: com.todoroo.astrid.data.Task): ByteArray {
suspend fun toVtodo(
calendar: CaldavCalendar,
caldavTask: CaldavTask,
task: com.todoroo.astrid.data.Task
): ByteArray {
var remoteModel: Task? = null
try {
if (!isNullOrEmpty(caldavTask.vtodo)) {
remoteModel = fromVtodo(caldavTask.vtodo!!)
val vtodo = vtodoCache.getVtodo(calendar, caldavTask)
if (vtodo?.isNotBlank() == true) {
remoteModel = fromVtodo(vtodo)
}
} catch (e: java.lang.Exception) {
Timber.e(e)
@ -180,7 +186,7 @@ class iCalendar @Inject constructor(
}
val caldavTask = existing ?: CaldavTask(task.id, calendar.uuid, remote.uid, obj)
val dirty = task.modificationDate > caldavTask.lastSync || caldavTask.lastSync == 0L
val local = caldavTask.vtodo?.let { fromVtodo(it) }
val local = vtodoCache.getVtodo(calendar, caldavTask)?.let { fromVtodo(it) }
task.applyRemote(remote, local)
caldavTask.applyRemote(remote, local)
@ -207,7 +213,7 @@ class iCalendar @Inject constructor(
task.suppressSync()
task.suppressRefresh()
taskDao.save(task)
caldavTask.vtodo = vtodo
vtodoCache.putVtodo(calendar, caldavTask, vtodo)
caldavTask.etag = eTag
if (!dirty) {
caldavTask.lastSync = task.modificationDate

@ -1,6 +1,10 @@
package org.tasks.data
import androidx.room.*
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import com.todoroo.andlib.data.Table
import com.todoroo.astrid.helper.UUIDHelper
@ -34,9 +38,6 @@ class CaldavTask {
@ColumnInfo(name = "cd_deleted")
var deleted: Long = 0
@ColumnInfo(name = "cd_vtodo")
var vtodo: String? = null
@ColumnInfo(name = "cd_remote_parent")
var remoteParent: String? = null
@ -65,7 +66,7 @@ class CaldavTask {
fun isDeleted() = deleted > 0
override fun toString(): String =
"CaldavTask(id=$id, task=$task, calendar=$calendar, `object`=$`object`, remoteId=$remoteId, etag=$etag, lastSync=$lastSync, deleted=$deleted, vtodo=$vtodo, remoteParent=$remoteParent, order=$order)"
"CaldavTask(id=$id, task=$task, calendar=$calendar, `object`=$`object`, remoteId=$remoteId, etag=$etag, lastSync=$lastSync, deleted=$deleted, remoteParent=$remoteParent, order=$order)"
companion object {
const val KEY = "caldav"

@ -17,9 +17,6 @@ class CaldavTaskContainer {
val isDeleted: Boolean
get() = task.isDeleted
val vtodo: String?
get() = caldavTask.vtodo
val sortOrder: Long
get() = caldavTask.order ?: DateTime(task.creationDate).toAppleEpoch()

@ -7,7 +7,6 @@ import androidx.room.Transaction
import org.tasks.data.CaldavDao.Companion.LOCAL
import org.tasks.db.SuspendDbUtils.chunkedMap
import org.tasks.db.SuspendDbUtils.eachChunk
import java.util.*
@Dao
abstract class DeletionDao {

@ -10,8 +10,6 @@ SELECT task.*, caldav_task.*
FROM tasks AS task
INNER JOIN caldav_tasks AS caldav_task ON _id = cd_task
WHERE cd_deleted = 0
AND cd_vtodo IS NOT NULL
AND cd_vtodo != ''
""")
suspend fun tasksWithVtodos(): List<CaldavTaskContainer>

@ -9,11 +9,13 @@ import com.todoroo.astrid.data.Task.Companion.NOTIFY_AT_DEADLINE
import com.todoroo.astrid.data.Task.Companion.NOTIFY_AT_START
import com.todoroo.astrid.data.Task.Companion.NOTIFY_MODE_FIVE
import com.todoroo.astrid.data.Task.Companion.NOTIFY_MODE_NONSTOP
import org.tasks.caldav.FileStorage
import org.tasks.data.Alarm.Companion.TYPE_RANDOM
import org.tasks.data.Alarm.Companion.TYPE_REL_END
import org.tasks.data.Alarm.Companion.TYPE_REL_START
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.extensions.getString
import timber.log.Timber
import java.util.concurrent.TimeUnit.HOURS
@ -443,7 +445,35 @@ object Migrations {
}
}
val MIGRATIONS = arrayOf(
@Suppress("FunctionName")
private fun migration_81_82(fileStorage: FileStorage) = object : Migration(81, 82) {
override fun migrate(database: SupportSQLiteDatabase) {
database
.query("SELECT `cdl_account`, `cd_calendar`, `cd_object`, `cd_vtodo` FROM `caldav_tasks` INNER JOIN `caldav_lists` ON `cdl_uuid` = `cd_calendar`")
.use {
while (it.moveToNext()) {
val file = fileStorage.getFile(
it.getString("cdl_account"),
it.getString("cd_calendar"),
it.getString("cd_object"),
) ?: continue
fileStorage.write(file, it.getString("cd_vtodo"))
}
}
database.execSQL("ALTER TABLE `caldav_tasks` RENAME TO `caldav_tasks-temp`")
database.execSQL(
"CREATE TABLE IF NOT EXISTS `caldav_tasks` (`cd_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `cd_task` INTEGER NOT NULL, `cd_calendar` TEXT, `cd_object` TEXT, `cd_remote_id` TEXT, `cd_etag` TEXT, `cd_last_sync` INTEGER NOT NULL, `cd_deleted` INTEGER NOT NULL, `cd_remote_parent` TEXT, `cd_order` INTEGER)"
)
database.execSQL("DROP INDEX `cd_task`")
database.execSQL("CREATE INDEX IF NOT EXISTS `cd_task` ON `caldav_tasks` (`cd_task`)")
database.execSQL(
"INSERT INTO `caldav_tasks` (`cd_id`, `cd_task`, `cd_calendar`, `cd_object`, `cd_remote_id`, `cd_etag`, `cd_last_sync`, `cd_deleted`, `cd_remote_parent`, `cd_order`) SELECT `cd_id`, `cd_task`, `cd_calendar`, `cd_object`, `cd_remote_id`, `cd_etag`, `cd_last_sync`, `cd_deleted`, `cd_remote_parent`, `cd_order` FROM `caldav_tasks-temp`"
)
database.execSQL("DROP TABLE `caldav_tasks-temp`")
}
}
fun migrations(fileStorage: FileStorage) = arrayOf(
MIGRATION_35_36,
MIGRATION_36_37,
MIGRATION_37_38,
@ -481,6 +511,7 @@ object Migrations {
MIGRATION_78_79,
MIGRATION_79_80,
MIGRATION_80_81,
migration_81_82(fileStorage),
)
private fun noop(from: Int, to: Int): Migration = object : Migration(from, to) {

@ -19,6 +19,7 @@ import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.billing.Inventory
import org.tasks.caldav.VtodoCache
import org.tasks.caldav.iCalendar
import org.tasks.caldav.iCalendar.Companion.fromVtodo
import org.tasks.data.CaldavAccount
@ -35,7 +36,9 @@ class EtebaseSynchronizer @Inject constructor(
private val taskDeleter: TaskDeleter,
private val inventory: Inventory,
private val clientProvider: EtebaseClientProvider,
private val iCal: iCalendar) {
private val iCal: iCalendar,
private val vtodoCache: VtodoCache,
) {
companion object {
init {
prodId = ProdId("+//IDN tasks.org//android-" + BuildConfig.VERSION_CODE + "//EN")
@ -142,7 +145,10 @@ class EtebaseSynchronizer @Inject constructor(
for (caldavTask in caldavDao.getMoved(caldavCalendar.uuid!!)) {
client.deleteItem(collection, caldavTask)
?.let { changes.add(it) }
?: caldavDao.delete(caldavTask)
?: run {
vtodoCache.delete(caldavCalendar, caldavTask)
caldavDao.delete(caldavTask)
}
}
for (change in caldavDao.getCaldavTasksToPush(caldavCalendar.uuid!!)) {
val task = change.task
@ -154,7 +160,11 @@ class EtebaseSynchronizer @Inject constructor(
?: taskDeleter.delete(task)
} else {
changes.add(
client.updateItem(collection, caldavTask, iCal.toVtodo(caldavTask, task))
client.updateItem(
collection,
caldavTask,
iCal.toVtodo(caldavCalendar, caldavTask, task)
)
)
}
}
@ -178,6 +188,7 @@ class EtebaseSynchronizer @Inject constructor(
if (item.isDeleted) {
if (caldavTask != null) {
if (caldavTask.isDeleted()) {
vtodoCache.delete(caldavCalendar, caldavTask)
caldavDao.delete(caldavTask)
} else {
taskDeleter.delete(caldavTask.task)
@ -185,7 +196,7 @@ class EtebaseSynchronizer @Inject constructor(
}
} else if (isLocalChange) {
caldavTask?.let {
it.vtodo = vtodo
vtodoCache.putVtodo(caldavCalendar, it, vtodo)
it.lastSync = item.meta.mtime ?: currentTimeMillis()
caldavDao.update(it)
}

@ -0,0 +1,6 @@
package org.tasks.extensions
import android.database.Cursor
fun Cursor.getString(columnName: String): String? =
getColumnIndex(columnName).takeIf { it >= 0 }?.let { getString(it) }

@ -10,6 +10,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.caldav.FileStorage
import org.tasks.data.CaldavDao
import org.tasks.data.GoogleTaskListDao
import org.tasks.data.OpenTaskDao
@ -26,9 +27,13 @@ import javax.inject.Singleton
internal class ProductionModule {
@Provides
@Singleton
fun getAppDatabase(@ApplicationContext context: Context, preferences: Preferences): Database {
fun getAppDatabase(
@ApplicationContext context: Context,
preferences: Preferences,
fileStorage: FileStorage,
): Database {
val builder = Room.databaseBuilder(context, Database::class.java, Database.NAME)
.addMigrations(*Migrations.MIGRATIONS)
.addMigrations(*Migrations.migrations(fileStorage))
if (!BuildConfig.DEBUG || !preferences.getBoolean(R.string.p_crash_main_queries, false)) {
builder.allowMainThreadQueries()
}

Loading…
Cancel
Save