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/TaskDao.kt

268 lines
11 KiB
Kotlin

package org.tasks.data
import androidx.paging.DataSource
import androidx.room.*
import androidx.sqlite.db.SimpleSQLiteQuery
import com.todoroo.andlib.sql.Criterion
import com.todoroo.andlib.sql.Field
import com.todoroo.andlib.sql.Functions
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.PermaSql
import com.todoroo.astrid.dao.Database
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.data.Task.Companion.NO_ID
import com.todoroo.astrid.helper.UUIDHelper
import org.tasks.BuildConfig
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
import org.tasks.db.SuspendDbUtils.chunkedMap
import org.tasks.db.SuspendDbUtils.eachChunk
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils.currentTimeMillis
import timber.log.Timber
@Dao
abstract class TaskDao(private val database: Database) {
@Query("SELECT * FROM tasks WHERE completed = 0 AND deleted = 0 AND (hideUntil > :now OR dueDate > :now)")
internal abstract suspend fun needsRefresh(now: Long = now()): List<Task>
@Query("SELECT * FROM tasks WHERE _id = :id LIMIT 1")
abstract suspend fun fetch(id: Long): Task?
suspend fun fetch(ids: List<Long>): List<Task> = ids.chunkedMap(this::fetchInternal)
@Query("SELECT * FROM tasks WHERE _id IN (:ids)")
internal abstract suspend fun fetchInternal(ids: List<Long>): List<Task>
@Query("SELECT COUNT(1) FROM tasks WHERE timerStart > 0 AND deleted = 0")
abstract suspend fun activeTimers(): Int
@Query("SELECT COUNT(1) FROM tasks INNER JOIN alarms ON tasks._id = alarms.task WHERE type = $TYPE_SNOOZE")
abstract suspend fun snoozedReminders(): Int
@Query("SELECT COUNT(1) FROM tasks INNER JOIN notification ON tasks._id = notification.task")
abstract suspend fun hasNotifications(): Int
@Query("SELECT tasks.* FROM tasks INNER JOIN notification ON tasks._id = notification.task")
abstract suspend fun activeNotifications(): List<Task>
@Query("SELECT * FROM tasks WHERE remoteId = :remoteId")
abstract suspend fun fetch(remoteId: String): Task?
@Query("SELECT * FROM tasks WHERE completed = 0 AND deleted = 0")
abstract suspend fun getActiveTasks(): List<Task>
@Query("SELECT * FROM tasks WHERE remoteId IN (:remoteIds) "
+ "AND recurrence IS NOT NULL AND LENGTH(recurrence) > 0")
abstract suspend fun getRecurringTasks(remoteIds: List<String>): List<Task>
@Query("UPDATE tasks SET completed = :completionDate, modified = :updateTime WHERE remoteId IN (:remoteIds)")
abstract suspend fun setCompletionDate(remoteIds: List<String>, completionDate: Long, updateTime: Long = now())
@Query("SELECT tasks.* FROM tasks "
+ "LEFT JOIN caldav_tasks ON tasks._id = caldav_tasks.cd_task "
+ "LEFT JOIN caldav_lists ON caldav_tasks.cd_calendar = caldav_lists.cdl_uuid "
+ "WHERE cdl_account = :account "
+ "AND (tasks.modified > caldav_tasks.cd_last_sync OR caldav_tasks.cd_remote_id = '' OR caldav_tasks.cd_remote_id IS NULL OR caldav_tasks.cd_deleted > 0) "
+ "ORDER BY CASE WHEN parent = 0 THEN 0 ELSE 1 END, `order` ASC")
abstract suspend fun getGoogleTasksToPush(account: String): List<Task>
@Query("""
SELECT tasks.*
FROM tasks
INNER JOIN caldav_tasks ON tasks._id = caldav_tasks.cd_task
WHERE caldav_tasks.cd_calendar = :calendar
AND cd_deleted = 0
AND (tasks.modified > caldav_tasks.cd_last_sync OR caldav_tasks.cd_last_sync = 0)
ORDER BY created""")
abstract suspend fun getCaldavTasksToPush(calendar: String): List<Task>
// --- SQL clause generators
@Query("SELECT * FROM tasks")
abstract suspend fun getAll(): List<Task>
@Query("SELECT calendarUri FROM tasks " + "WHERE calendarUri IS NOT NULL AND calendarUri != ''")
abstract suspend fun getAllCalendarEvents(): List<String>
@Query("UPDATE tasks SET calendarUri = '' " + "WHERE calendarUri IS NOT NULL AND calendarUri != ''")
abstract suspend fun clearAllCalendarEvents(): Int
@Query("SELECT calendarUri FROM tasks "
+ "WHERE completed > 0 AND calendarUri IS NOT NULL AND calendarUri != ''")
abstract suspend fun getCompletedCalendarEvents(): List<String>
@Query("UPDATE tasks SET calendarUri = '' "
+ "WHERE completed > 0 AND calendarUri IS NOT NULL AND calendarUri != ''")
abstract suspend fun clearCompletedCalendarEvents(): Int
open suspend fun fetchTasks(callback: suspend (SubtaskInfo) -> List<String>): List<TaskContainer> {
return fetchTasks(getSubtaskInfo(), callback)
}
open suspend fun fetchTasks(subtasks: SubtaskInfo, callback: suspend (SubtaskInfo) -> List<String>): List<TaskContainer> =
database.withTransaction {
val start = if (BuildConfig.DEBUG) now() else 0
val queries = callback(subtasks)
val last = queries.size - 1
for (i in 0 until last) {
query(SimpleSQLiteQuery(queries[i]))
}
val result = fetchTasks(SimpleSQLiteQuery(queries[last]))
Timber.v("%sms: %s", now() - start, queries.joinToString(";\n"))
result
}
suspend fun fetchTasks(preferences: Preferences, filter: Filter): List<TaskContainer> =
fetchTasks {
TaskListQuery.getQuery(preferences, filter, it)
}
@RawQuery
internal abstract suspend fun query(query: SimpleSQLiteQuery): Int
@RawQuery
internal abstract suspend fun fetchTasks(query: SimpleSQLiteQuery): List<TaskContainer>
@RawQuery
abstract suspend fun count(query: SimpleSQLiteQuery): Int
@Query("SELECT EXISTS(SELECT 1 FROM tasks WHERE parent > 0 AND deleted = 0) AS hasSubtasks")
abstract suspend fun getSubtaskInfo(): SubtaskInfo
@RawQuery(observedEntities = [Place::class])
abstract fun getTaskFactory(query: SimpleSQLiteQuery): DataSource.Factory<Int, TaskContainer>
suspend fun touch(ids: List<Long>, now: Long = currentTimeMillis()) =
ids.eachChunk { internalTouch(it, now) }
@Query("UPDATE tasks SET modified = :now WHERE _id in (:ids)")
internal abstract suspend fun internalTouch(ids: List<Long>, now: Long = currentTimeMillis())
@Query("UPDATE tasks SET `order` = :order WHERE _id = :id")
internal abstract suspend fun setOrder(id: Long, order: Long?)
suspend fun setParent(parent: Long, tasks: List<Long>) =
tasks.eachChunk { setParentInternal(parent, it) }
@Query("UPDATE tasks SET parent = :parent WHERE _id IN (:children) AND _id != :parent")
internal abstract suspend fun setParentInternal(parent: Long, children: List<Long>)
@Query("UPDATE tasks SET lastNotified = :timestamp WHERE _id = :id AND lastNotified != :timestamp")
abstract suspend fun setLastNotified(id: Long, timestamp: Long)
suspend fun getChildren(id: Long): List<Long> = getChildren(listOf(id))
@Query("""
WITH RECURSIVE recursive_tasks (task) AS (
SELECT _id
FROM tasks
WHERE parent IN (:ids)
UNION ALL
SELECT _id
FROM tasks
INNER JOIN recursive_tasks ON recursive_tasks.task = tasks.parent
WHERE tasks.deleted = 0)
SELECT task
FROM recursive_tasks
""")
abstract suspend fun getChildren(ids: List<Long>): List<Long>
@Query("""
WITH RECURSIVE recursive_tasks (task, parent) AS (
SELECT _id, parent FROM tasks WHERE _id = :parent
UNION ALL
SELECT _id, tasks.parent FROM tasks
INNER JOIN recursive_tasks ON recursive_tasks.parent = tasks._id
WHERE tasks.deleted = 0
)
SELECT task
FROM recursive_tasks
""")
abstract suspend fun getParents(parent: Long): List<Long>
internal suspend fun setCollapsed(preferences: Preferences, filter: Filter, collapsed: Boolean) {
fetchTasks(preferences, filter)
.filter(TaskContainer::hasChildren)
.map(TaskContainer::getId)
.eachChunk { setCollapsed(it, collapsed) }
}
@Query("UPDATE tasks SET collapsed = :collapsed, modified = :now WHERE _id IN (:ids)")
internal abstract suspend fun setCollapsed(ids: List<Long>, collapsed: Boolean, now: Long = now())
@Insert
abstract suspend fun insert(task: Task): Long
suspend fun update(task: Task, original: Task? = null): Boolean {
if (!task.insignificantChange(original)) {
task.modificationDate = now()
}
return updateInternal(task) == 1
}
@Update
internal abstract suspend fun updateInternal(task: Task): Int
suspend fun createNew(task: Task): Long {
task.id = NO_ID
if (task.creationDate == 0L) {
task.creationDate = now()
}
if (Task.isUuidEmpty(task.remoteId)) {
task.remoteId = UUIDHelper.newUUID()
}
if (BuildConfig.DEBUG) {
require(task.remoteId?.isNotBlank() == true && task.remoteId != "0")
}
val insert = insert(task)
task.id = insert
return task.id
}
suspend fun count(filter: Filter): Int {
val query = getQuery(filter.sqlQuery, Field.COUNT)
val start = if (BuildConfig.DEBUG) now() else 0
val count = count(query)
Timber.v("%sms: %s", now() - start, query.sql)
return count
}
suspend fun fetchFiltered(filter: Filter): List<Task> = fetchFiltered(filter.getSqlQuery())
suspend fun fetchFiltered(queryTemplate: String): List<Task> {
val query = getQuery(queryTemplate, Task.FIELDS)
val start = if (BuildConfig.DEBUG) now() else 0
val tasks = fetchTasks(query)
Timber.v("%sms: %s", now() - start, query.sql)
return tasks.map(TaskContainer::getTask)
}
@Query("""
SELECT _id
FROM tasks
LEFT JOIN caldav_tasks ON _id = cd_task AND cd_deleted = 0
WHERE cd_id IS NULL
AND parent = 0
""")
abstract suspend fun getLocalTasks(): List<Long>
/** Generates SQL clauses */
object TaskCriteria {
/** @return tasks that have not yet been completed or deleted
*/
@JvmStatic fun activeAndVisible(): Criterion = Criterion.and(
Task.COMPLETION_DATE.lte(0),
Task.DELETION_DATE.lte(0),
Task.HIDE_UNTIL.lte(Functions.now()))
}
companion object {
fun getQuery(queryTemplate: String, vararg fields: Field): SimpleSQLiteQuery =
SimpleSQLiteQuery(
com.todoroo.andlib.sql.Query.select(*fields)
.withQueryTemplate(PermaSql.replacePlaceholdersForQuery(queryTemplate))
.from(Task.TABLE)
.toString())
}
}