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 @Query("SELECT * FROM tasks WHERE _id = :id LIMIT 1") abstract suspend fun fetch(id: Long): Task? suspend fun fetch(ids: List): List = ids.chunkedMap(this::fetchInternal) @Query("SELECT * FROM tasks WHERE _id IN (:ids)") internal abstract suspend fun fetchInternal(ids: List): List @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 @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 @Query("SELECT * FROM tasks WHERE remoteId IN (:remoteIds) " + "AND recurrence IS NOT NULL AND LENGTH(recurrence) > 0") abstract suspend fun getRecurringTasks(remoteIds: List): List @Query("UPDATE tasks SET completed = :completionDate, modified = :updateTime WHERE remoteId IN (:remoteIds)") abstract suspend fun setCompletionDate(remoteIds: List, 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 @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 // --- SQL clause generators @Query("SELECT * FROM tasks") abstract suspend fun getAll(): List @Query("SELECT calendarUri FROM tasks " + "WHERE calendarUri IS NOT NULL AND calendarUri != ''") abstract suspend fun getAllCalendarEvents(): List @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 @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): List { return fetchTasks(getSubtaskInfo(), callback) } open suspend fun fetchTasks(subtasks: SubtaskInfo, callback: suspend (SubtaskInfo) -> List): List = 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 = fetchTasks { TaskListQuery.getQuery(preferences, filter, it) } @RawQuery internal abstract suspend fun query(query: SimpleSQLiteQuery): Int @RawQuery internal abstract suspend fun fetchTasks(query: SimpleSQLiteQuery): List @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 suspend fun touch(ids: List, 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, 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) = 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) @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 = 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): List @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 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, 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 = fetchFiltered(filter.getSqlQuery()) suspend fun fetchFiltered(queryTemplate: String): List { 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 /** 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()) } }