You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tasks/app/src/main/java/org/tasks/data/CaldavDao.kt

413 lines
17 KiB
Kotlin

package org.tasks.data
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.api.Filter.Companion.NO_ORDER
import com.todoroo.astrid.core.SortHelper.APPLE_EPOCH
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.helper.UUIDHelper
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.tasks.R
import org.tasks.data.CaldavAccount.Companion.TYPE_ETESYNC
import org.tasks.data.CaldavAccount.Companion.TYPE_GOOGLE_TASKS
import org.tasks.data.CaldavAccount.Companion.TYPE_LOCAL
import org.tasks.data.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.CaldavAccount.Companion.TYPE_TASKS
import org.tasks.date.DateTimeUtils.toAppleEpoch
import org.tasks.db.DbUtils.dbchunk
import org.tasks.db.SuspendDbUtils.chunkedMap
import org.tasks.filters.CaldavFilters
import org.tasks.time.DateTimeUtils.currentTimeMillis
@Dao
abstract class CaldavDao {
@Query("SELECT COUNT(*) FROM caldav_lists WHERE cdl_account = :account")
abstract fun listCount(account: String): Int
@Query("SELECT * FROM caldav_lists")
abstract fun subscribeToCalendars(): LiveData<List<CaldavCalendar>>
@Query("SELECT * FROM caldav_lists WHERE cdl_uuid = :uuid LIMIT 1")
abstract suspend fun getCalendarByUuid(uuid: String): CaldavCalendar?
@Query("SELECT * FROM caldav_lists WHERE cdl_id = :id LIMIT 1")
abstract suspend fun getCalendarById(id: Long): CaldavCalendar?
@Query("SELECT * FROM caldav_lists WHERE cdl_account = :uuid")
abstract suspend fun getCalendarsByAccount(uuid: String): List<CaldavCalendar>
@Query("SELECT * FROM caldav_accounts WHERE cda_uuid = :uuid LIMIT 1")
abstract suspend fun getAccountByUuid(uuid: String): CaldavAccount?
suspend fun getAccounts(vararg types: Int) = getAccounts(types.toList())
@Query("SELECT * FROM caldav_accounts WHERE cda_account_type IN (:types)")
abstract suspend fun getAccounts(types: List<Int>): List<CaldavAccount>
@Query("SELECT * FROM caldav_accounts WHERE cda_account_type = :type AND cda_username = :username")
abstract suspend fun getAccount(type: Int, username: String): CaldavAccount?
@Query("SELECT * FROM caldav_accounts WHERE cda_id = :id")
abstract fun watchAccount(id: Long): LiveData<CaldavAccount>
@Query("""
SELECT *
FROM caldav_accounts
WHERE cda_account_type != $TYPE_LOCAL
ORDER BY CASE cda_account_type
WHEN $TYPE_TASKS THEN 0
ELSE 1
END, UPPER(cda_name)
""")
abstract fun watchAccounts(): LiveData<List<CaldavAccount>>
@Query("""
SELECT *
FROM caldav_accounts
ORDER BY CASE cda_account_type
WHEN $TYPE_TASKS THEN 0
WHEN $TYPE_LOCAL THEN 2
ELSE 1
END, UPPER(cda_name)
""")
abstract suspend fun getAccounts(): List<CaldavAccount>
@Query("UPDATE caldav_accounts SET cda_collapsed = :collapsed WHERE cda_id = :id")
abstract suspend fun setCollapsed(id: Long, collapsed: Boolean)
@Insert
abstract suspend fun insert(caldavAccount: CaldavAccount): Long
@Update
abstract suspend fun update(caldavAccount: CaldavAccount)
suspend fun insert(caldavCalendar: CaldavCalendar) {
caldavCalendar.id = insertInternal(caldavCalendar)
}
@Insert
internal abstract suspend fun insertInternal(caldavCalendar: CaldavCalendar): Long
@Update
abstract suspend fun update(caldavCalendar: CaldavCalendar)
@Transaction
open suspend fun insert(task: Task, caldavTask: CaldavTask, addToTop: Boolean): Long {
if (task.order != null) {
return insert(caldavTask)
}
if (addToTop) {
task.order = findFirstTask(caldavTask.calendar!!, task.parent)
?.takeIf { task.creationDate.toAppleEpoch() >= it }
?.minus(1)
} else {
task.order = findLastTask(caldavTask.calendar!!, task.parent)
?.takeIf { task.creationDate.toAppleEpoch() <= it }
?.plus(1)
}
val id = insert(caldavTask)
update(task)
return id
}
@Query("""
SELECT MIN(IFNULL(`order`, (created - $APPLE_EPOCH) / 1000))
FROM caldav_tasks
INNER JOIN tasks ON _id = cd_task
WHERE cd_calendar = :calendar
AND cd_deleted = 0
AND deleted = 0
AND parent = :parent
""")
internal abstract suspend fun findFirstTask(calendar: String, parent: Long): Long?
@Query("""
SELECT MAX(IFNULL(`order`, (created - $APPLE_EPOCH) / 1000))
FROM caldav_tasks
INNER JOIN tasks ON _id = cd_task
WHERE cd_calendar = :calendar
AND cd_deleted = 0
AND deleted = 0
AND parent = :parent
""")
internal abstract suspend fun findLastTask(calendar: String, parent: Long): Long?
@Insert
abstract suspend fun insert(caldavTask: CaldavTask): Long
@Insert
abstract suspend fun insert(tasks: Iterable<CaldavTask>)
@Update
abstract suspend fun update(caldavTask: CaldavTask)
@Update
abstract suspend fun update(task: Task)
@Update
abstract suspend fun updateTasks(tasks: Iterable<Task>)
@Query("UPDATE caldav_tasks SET cd_remote_parent = :remoteParent WHERE cd_id = :id")
internal abstract suspend fun update(id: Long, remoteParent: String?)
@Update
abstract suspend fun update(tasks: Iterable<CaldavTask>)
@Delete
abstract suspend fun delete(caldavTask: CaldavTask)
@Delete
abstract suspend fun delete(caldavTasks: List<CaldavTask>)
@Query("SELECT * FROM caldav_tasks WHERE cd_deleted > 0 AND cd_calendar = :calendar")
abstract suspend fun getMoved(calendar: String): List<CaldavTask>
@Query("UPDATE caldav_tasks SET cd_deleted = :now WHERE cd_task IN (:tasks)")
abstract suspend fun markDeleted(tasks: List<Long>, now: Long = currentTimeMillis())
@Query("SELECT * FROM caldav_tasks WHERE cd_task = :taskId AND cd_deleted = 0 LIMIT 1")
abstract suspend fun getTask(taskId: Long): CaldavTask?
@Query("SELECT cd_remote_id FROM caldav_tasks WHERE cd_task = :taskId AND cd_deleted = 0")
abstract suspend fun getRemoteIdForTask(taskId: Long): String?
@Query("SELECT * FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_object = :obj LIMIT 1")
abstract suspend fun getTask(calendar: String, obj: String): CaldavTask?
@Query("SELECT * FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_remote_id = :remoteId")
abstract suspend fun getTaskByRemoteId(calendar: String, remoteId: String): CaldavTask?
@Query("SELECT * FROM caldav_tasks WHERE cd_task = :taskId")
abstract suspend fun getTasks(taskId: Long): List<CaldavTask>
@Query("""
SELECT EXISTS(SELECT 1
FROM caldav_tasks
INNER JOIN caldav_lists ON cdl_uuid = cd_calendar
INNER JOIN caldav_accounts ON cda_uuid = cdl_account
WHERE cd_task = :id
AND cda_account_type IN (:types))
""")
abstract suspend fun isAccountType(id: Long, types: List<Int>): Boolean
suspend fun getTasks(taskIds: List<Long>): List<CaldavTask> =
taskIds.chunkedMap { getTasksInternal(it) }
@Query("SELECT * FROM caldav_tasks WHERE cd_task in (:taskIds) AND cd_deleted = 0")
internal abstract suspend fun getTasksInternal(taskIds: List<Long>): List<CaldavTask>
@Query("SELECT task.*, caldav_task.* FROM tasks AS task "
+ "INNER JOIN caldav_tasks AS caldav_task ON _id = cd_task "
+ "WHERE cd_calendar = :calendar "
+ "AND modified > cd_last_sync "
+ "AND cd_deleted = 0")
abstract suspend fun getCaldavTasksToPush(calendar: String): List<CaldavTaskContainer>
@Query("SELECT * FROM caldav_lists " +
"INNER JOIN caldav_accounts ON caldav_lists.cdl_account = caldav_accounts.cda_uuid " +
"WHERE caldav_accounts.cda_account_type = $TYPE_GOOGLE_TASKS " +
"ORDER BY cdl_name COLLATE NOCASE")
abstract suspend fun getGoogleTaskLists(): List<CaldavCalendar>
@Query("SELECT * FROM caldav_lists " +
"INNER JOIN caldav_accounts ON caldav_lists.cdl_account = caldav_accounts.cda_uuid " +
"WHERE caldav_accounts.cda_account_type != $TYPE_GOOGLE_TASKS " +
"ORDER BY cdl_name COLLATE NOCASE")
abstract suspend fun getCalendars(): List<CaldavCalendar>
@Query("""
SELECT EXISTS(SELECT 1
FROM caldav_lists
INNER JOIN caldav_accounts ON cdl_account = cda_uuid
WHERE cda_account_type != $TYPE_OPENTASKS
AND cda_account_type != $TYPE_ETESYNC
AND cdl_url IN (:urls))
""")
abstract suspend fun anyExist(urls: List<String>): Boolean
@Query("SELECT * FROM caldav_lists WHERE cdl_uuid = :uuid LIMIT 1")
abstract suspend fun getCalendar(uuid: String): CaldavCalendar?
@Query("SELECT cd_object FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_deleted = 0 AND cd_last_sync > 0")
abstract suspend fun getRemoteObjects(calendar: String): List<String>
@Query("SELECT cd_remote_id FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_deleted = 0 AND cd_last_sync > 0")
abstract suspend fun getRemoteIds(calendar: String): List<String>
suspend fun getTasksByRemoteId(calendar: String, remoteIds: List<String>): List<Long> =
remoteIds.chunkedMap { getTasksByRemoteIdInternal(calendar, it) }
@Query("SELECT cd_task FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_remote_id IN (:remoteIds)")
internal abstract suspend fun getTasksByRemoteIdInternal(calendar: String, remoteIds: List<String>): List<Long>
suspend fun getTasks(calendar: String, objects: List<String>): List<Long> =
objects.chunkedMap { getTasksInternal(calendar, it) }
@Query("SELECT cd_task FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_object IN (:objects)")
internal abstract suspend fun getTasksInternal(calendar: String, objects: List<String>): List<Long>
@Query("SELECT * FROM caldav_lists WHERE cdl_account = :account AND cdl_url NOT IN (:urls)")
abstract suspend fun findDeletedCalendars(account: String, urls: List<String>): List<CaldavCalendar>
@Query("SELECT * FROM caldav_lists WHERE cdl_account = :account AND cdl_url = :url LIMIT 1")
abstract suspend fun getCalendarByUrl(account: String, url: String): CaldavCalendar?
@Query("SELECT caldav_accounts.* from caldav_accounts"
+ " INNER JOIN caldav_tasks ON cd_task = :task"
+ " INNER JOIN caldav_lists ON cd_calendar = cdl_uuid"
+ " WHERE cdl_account = cda_uuid")
abstract suspend fun getAccountForTask(task: Long): CaldavAccount?
@Query("SELECT DISTINCT cd_calendar FROM caldav_tasks WHERE cd_deleted = 0 AND cd_task IN (:tasks)")
abstract suspend fun getCalendars(tasks: List<Long>): List<String>
@Query("""
SELECT caldav_lists.*, COUNT(DISTINCT(tasks._id)) AS count, COUNT(DISTINCT(principal_access.id)) AS principals
FROM caldav_lists
LEFT JOIN caldav_tasks ON caldav_tasks.cd_calendar = caldav_lists.cdl_uuid
LEFT JOIN tasks ON caldav_tasks.cd_task = tasks._id AND
tasks.deleted = 0 AND
tasks.completed = 0 AND
tasks.hideUntil < :now AND
cd_deleted = 0
LEFT JOIN principal_access ON caldav_lists.cdl_id = principal_access.list
LEFT JOIN caldav_accounts ON caldav_accounts.cda_uuid = caldav_lists.cdl_account
WHERE caldav_lists.cdl_account = :uuid
AND caldav_accounts.cda_account_type != $TYPE_GOOGLE_TASKS
GROUP BY caldav_lists.cdl_uuid
""")
abstract suspend fun getCaldavFilters(uuid: String, now: Long = currentTimeMillis()): List<CaldavFilters>
@Query("UPDATE tasks SET parent = IFNULL(("
+ " SELECT p.cd_task FROM caldav_tasks AS p"
+ " INNER JOIN caldav_tasks ON caldav_tasks.cd_task = tasks._id"
+ " WHERE p.cd_remote_id = caldav_tasks.cd_remote_parent"
+ " AND p.cd_calendar = caldav_tasks.cd_calendar"
+ " AND p.cd_deleted = 0"
+ " AND caldav_tasks.cd_remote_parent IS NOT NULL"
+ " AND caldav_tasks.cd_remote_parent != ''"
+ "), 0)"
+ "WHERE _id IN (SELECT _id FROM tasks INNER JOIN caldav_tasks ON _id = cd_task WHERE cd_deleted = 0)")
abstract suspend fun updateParents()
@Query("UPDATE tasks SET parent = IFNULL(("
+ " SELECT p.cd_task FROM caldav_tasks AS p"
+ " INNER JOIN caldav_tasks "
+ " ON caldav_tasks.cd_task = tasks._id"
+ " AND caldav_tasks.cd_calendar = :calendar"
+ " WHERE p.cd_remote_id = caldav_tasks.cd_remote_parent"
+ " AND p.cd_calendar = caldav_tasks.cd_calendar"
+ " AND p.cd_deleted = 0"
+ " AND caldav_tasks.cd_remote_parent IS NOT NULL"
+ " AND caldav_tasks.cd_remote_parent != ''"
+ "), 0)"
+ "WHERE _id IN (SELECT _id FROM tasks INNER JOIN caldav_tasks ON _id = cd_task WHERE cd_deleted = 0 AND cd_calendar = :calendar)")
abstract suspend fun updateParents(calendar: String)
@Transaction
open suspend fun move(
task: TaskContainer,
previousParent: Long,
newParent: Long,
newPosition: Long?,
) {
val previousPosition = task.caldavSortOrder
if (newPosition != null) {
if (newParent == previousParent && newPosition < previousPosition) {
shiftDown(task.caldav!!, newParent, newPosition, previousPosition)
} else {
val list = newParent.takeIf { it > 0 }?.let { getTask(it)?.calendar } ?: task.caldav!!
shiftDown(list, newParent, newPosition)
}
}
task.task.order = newPosition
setTaskOrder(task.id, newPosition)
}
@Transaction
open suspend fun shiftDown(calendar: String, parent: Long, from: Long, to: Long? = null) {
val updated = ArrayList<Task>()
val tasks = getTasksToShift(calendar, parent, from, to)
for (i in tasks.indices) {
val task = tasks[i]
val current = from + i
if (task.sortOrder == current) {
val task = task.task
task.order = current + 1
updated.add(task)
} else if (task.sortOrder > current) {
break
}
}
updateTasks(updated)
updated
.map(Task::id)
.dbchunk()
.forEach { touchInternal(it) }
}
@Query("UPDATE tasks SET modified = :modificationTime WHERE _id in (:ids)")
internal abstract suspend fun touchInternal(ids: List<Long>, modificationTime: Long = now())
@Query("""
SELECT task.*, caldav_task.*, IFNULL(`order`, (created - $APPLE_EPOCH) / 1000) AS primary_sort
FROM caldav_tasks AS caldav_task
INNER JOIN tasks AS task ON _id = cd_task
WHERE cd_calendar = :calendar
AND parent = :parent
AND cd_deleted = 0
AND deleted = 0
AND primary_sort >= :from
AND primary_sort < IFNULL(:to, ${Long.MAX_VALUE})
ORDER BY primary_sort
""")
internal abstract suspend fun getTasksToShift(calendar: String, parent: Long, from: Long, to: Long?): List<CaldavTaskContainer>
@Query("UPDATE caldav_lists SET cdl_order = $NO_ORDER")
abstract suspend fun resetOrders()
@Query("UPDATE caldav_lists SET cdl_order = :order WHERE cdl_id = :id")
abstract suspend fun setOrder(id: Long, order: Int)
@Query("UPDATE tasks SET `order` = :order WHERE _id = :id")
abstract suspend fun setTaskOrder(id: Long, order: Long?)
suspend fun setupLocalAccount(context: Context): CaldavAccount = mutex.withLock {
val account = getLocalAccount()
getLocalList(context, account)
return account
}
suspend fun getLocalList(context: Context) = mutex.withLock {
getLocalList(context, getLocalAccount())
}
private suspend fun getLocalAccount() = getAccountByUuid(LOCAL) ?: CaldavAccount().apply {
accountType = TYPE_LOCAL
uuid = LOCAL
id = insert(this)
}
private suspend fun getLocalList(context: Context, account: CaldavAccount): CaldavCalendar =
getCalendarsByAccount(account.uuid!!).getOrNull(0)
?: CaldavCalendar(
name = context.getString(R.string.default_list),
uuid = UUIDHelper.newUUID(),
account = account.uuid,
).apply {
insert(this)
}
companion object {
const val LOCAL = "local"
private val mutex = Mutex()
}
}