diff --git a/app/src/androidTest/java/com/todoroo/astrid/service/TaskMoverTest.kt b/app/src/androidTest/java/com/todoroo/astrid/service/TaskMoverTest.kt index e13c1912d..325afa5c1 100644 --- a/app/src/androidTest/java/com/todoroo/astrid/service/TaskMoverTest.kt +++ b/app/src/androidTest/java/com/todoroo/astrid/service/TaskMoverTest.kt @@ -9,7 +9,6 @@ import dagger.hilt.android.testing.UninstallModules import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Test import org.tasks.data.CaldavCalendar import org.tasks.data.CaldavDao @@ -43,12 +42,6 @@ class TaskMoverTest : InjectingTestCase() { @Inject lateinit var caldavDao: CaldavDao @Inject lateinit var taskMover: TaskMover - @Before - override fun setUp() { - super.setUp() - taskDaoAsync.initialize(workManager) - } - @Test fun moveBetweenGoogleTaskLists() = runBlocking { createTasks(1) diff --git a/app/src/main/java/com/todoroo/astrid/dao/Database.kt b/app/src/main/java/com/todoroo/astrid/dao/Database.kt index 22b42a569..d3484c312 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/Database.kt +++ b/app/src/main/java/com/todoroo/astrid/dao/Database.kt @@ -4,6 +4,7 @@ import androidx.room.Database import androidx.room.RoomDatabase import com.todoroo.astrid.data.Task import org.tasks.data.* +import org.tasks.data.TaskDao import org.tasks.notifications.Notification import org.tasks.notifications.NotificationDao diff --git a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt index 26905b97c..8f04f8e72 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt +++ b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt @@ -6,204 +6,107 @@ package com.todoroo.astrid.dao 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 import com.todoroo.astrid.api.Filter -import com.todoroo.astrid.api.PermaSql 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.Place import org.tasks.data.SubtaskInfo import org.tasks.data.TaskContainer -import org.tasks.data.TaskListQuery -import org.tasks.db.SuspendDbUtils.chunkedMap +import org.tasks.data.TaskDao import org.tasks.db.SuspendDbUtils.eachChunk import org.tasks.jobs.WorkManager import org.tasks.preferences.Preferences -import org.tasks.time.DateTimeUtils.currentTimeMillis -import timber.log.Timber +import javax.inject.Inject -@Dao -abstract class TaskDao(private val database: Database) { - private lateinit var workManager: WorkManager +class TaskDao @Inject constructor( + private val workManager: WorkManager, + private val taskDao: TaskDao) { - fun initialize(workManager: WorkManager) { - this.workManager = workManager - } - - @Query("SELECT * FROM tasks WHERE completed = 0 AND deleted = 0 AND (hideUntil > :now OR dueDate > :now)") - internal abstract suspend fun needsRefresh(now: Long = DateUtilities.now()): List - - @Query("SELECT * FROM tasks WHERE _id = :id LIMIT 1") - abstract suspend fun fetch(id: Long): Task? + internal suspend fun needsRefresh(now: Long = DateUtilities.now()): List = + taskDao.needsRefresh(now) - suspend fun fetch(ids: List): List = ids.chunkedMap(this::fetchInternal) + suspend fun fetch(id: Long): Task? = taskDao.fetch(id) - @Query("SELECT * FROM tasks WHERE _id IN (:ids)") - internal abstract suspend fun fetchInternal(ids: List): List + suspend fun fetch(ids: List): List = taskDao.fetch(ids) - @Query("SELECT COUNT(1) FROM tasks WHERE timerStart > 0 AND deleted = 0") - abstract suspend fun activeTimers(): Int + suspend fun activeTimers(): Int = taskDao.activeTimers() - @Query("SELECT tasks.* FROM tasks INNER JOIN notification ON tasks._id = notification.task") - abstract suspend fun activeNotifications(): List + suspend fun activeNotifications(): List = taskDao.activeNotifications() - @Query("SELECT * FROM tasks WHERE remoteId = :remoteId") - abstract suspend fun fetch(remoteId: String): Task? + suspend fun fetch(remoteId: String): Task? = taskDao.fetch(remoteId) - @Query("SELECT * FROM tasks WHERE completed = 0 AND deleted = 0") - abstract suspend fun getActiveTasks(): List + suspend fun getActiveTasks(): List = taskDao.getActiveTasks() - @Query("SELECT * FROM tasks WHERE remoteId IN (:remoteIds) " - + "AND recurrence IS NOT NULL AND LENGTH(recurrence) > 0") - abstract suspend fun getRecurringTasks(remoteIds: List): List + suspend fun getRecurringTasks(remoteIds: List): List = + taskDao.getRecurringTasks(remoteIds) - @Query("UPDATE tasks SET completed = :completionDate " + "WHERE remoteId = :remoteId") - abstract suspend fun setCompletionDate(remoteId: String, completionDate: Long) + suspend fun setCompletionDate(remoteId: String, completionDate: Long) = + taskDao.setCompletionDate(remoteId, completionDate) - @Query("UPDATE tasks SET snoozeTime = :millis WHERE _id in (:taskIds)") - abstract suspend fun snooze(taskIds: List, millis: Long) + suspend fun snooze(taskIds: List, millis: Long) = taskDao.snooze(taskIds, millis) - @Query("SELECT tasks.* FROM tasks " - + "LEFT JOIN google_tasks ON tasks._id = google_tasks.gt_task " - + "WHERE gt_list_id IN (SELECT gtl_remote_id FROM google_task_lists WHERE gtl_account = :account)" - + "AND (tasks.modified > google_tasks.gt_last_sync OR google_tasks.gt_remote_id = '' OR google_tasks.gt_deleted > 0) " - + "ORDER BY CASE WHEN gt_parent = 0 THEN 0 ELSE 1 END, gt_order ASC") - abstract suspend fun getGoogleTasksToPush(account: String): List + suspend fun getGoogleTasksToPush(account: String): List = + taskDao.getGoogleTasksToPush(account) - @Query(""" - SELECT tasks.* - FROM tasks - INNER JOIN caldav_tasks ON tasks._id = caldav_tasks.cd_task - WHERE caldav_tasks.cd_calendar = :calendar - AND (tasks.modified > caldav_tasks.cd_last_sync OR caldav_tasks.cd_last_sync = 0)""") - abstract suspend fun getCaldavTasksToPush(calendar: String): List + suspend fun getCaldavTasksToPush(calendar: String): List = + taskDao.getCaldavTasksToPush(calendar) - @Query("SELECT * FROM TASKS " - + "WHERE completed = 0 AND deleted = 0 AND (notificationFlags > 0 OR notifications > 0)") - abstract suspend fun getTasksWithReminders(): List + suspend fun getTasksWithReminders(): List = taskDao.getTasksWithReminders() - // --- SQL clause generators - @Query("SELECT * FROM tasks") - abstract suspend fun getAll(): List + suspend fun getAll(): List = taskDao.getAll() - @Query("SELECT calendarUri FROM tasks " + "WHERE calendarUri IS NOT NULL AND calendarUri != ''") - abstract suspend fun getAllCalendarEvents(): List + suspend fun getAllCalendarEvents(): List = taskDao.getAllCalendarEvents() - @Query("UPDATE tasks SET calendarUri = '' " + "WHERE calendarUri IS NOT NULL AND calendarUri != ''") - abstract suspend fun clearAllCalendarEvents(): Int + suspend fun clearAllCalendarEvents(): Int = taskDao.clearAllCalendarEvents() - @Query("SELECT calendarUri FROM tasks " - + "WHERE completed > 0 AND calendarUri IS NOT NULL AND calendarUri != ''") - abstract suspend fun getCompletedCalendarEvents(): List + suspend fun getCompletedCalendarEvents(): List = taskDao.getCompletedCalendarEvents() - @Query("UPDATE tasks SET calendarUri = '' " - + "WHERE completed > 0 AND calendarUri IS NOT NULL AND calendarUri != ''") - abstract suspend fun clearCompletedCalendarEvents(): Int + suspend fun clearCompletedCalendarEvents(): Int = taskDao.clearCompletedCalendarEvents() - @Transaction - open suspend fun fetchTasks(callback: suspend (SubtaskInfo) -> List): List { - return fetchTasks(callback, getSubtaskInfo()) - } + suspend fun fetchTasks(callback: suspend (SubtaskInfo) -> List): List = + taskDao.fetchTasks(callback) - @Transaction - open suspend fun fetchTasks(callback: suspend (SubtaskInfo) -> List, subtasks: SubtaskInfo): List { - val start = if (BuildConfig.DEBUG) DateUtilities.now() else 0 - val queries = callback.invoke(subtasks) - val db = database.openHelper.writableDatabase - val last = queries.size - 1 - for (i in 0 until last) { - db.execSQL(queries[i]) - } - val result = fetchTasks(SimpleSQLiteQuery(queries[last])) - Timber.v("%sms: %s", DateUtilities.now() - start, queries.joinToString(";\n")) - return result - } + suspend fun fetchTasks(callback: suspend (SubtaskInfo) -> List, subtasks: SubtaskInfo): List = + taskDao.fetchTasks(callback, subtasks) - suspend fun fetchTasks(preferences: Preferences, filter: Filter): List { - return fetchTasks { - TaskListQuery.getQuery(preferences, filter, it) - } - } + suspend fun fetchTasks(preferences: Preferences, filter: Filter): List = + taskDao.fetchTasks(preferences, filter) - @RawQuery - abstract suspend fun fetchTasks(query: SimpleSQLiteQuery): List + suspend fun fetchTasks(query: SimpleSQLiteQuery): List = + taskDao.fetchTasks(query) - @RawQuery - abstract suspend fun count(query: SimpleSQLiteQuery): Int + suspend fun count(query: SimpleSQLiteQuery): Int = taskDao.count(query) - @Query(""" -SELECT EXISTS(SELECT 1 FROM tasks WHERE parent > 0 AND deleted = 0) AS hasSubtasks, - EXISTS(SELECT 1 - FROM google_tasks - INNER JOIN tasks ON gt_task = _id - WHERE deleted = 0 - AND gt_parent > 0 - AND gt_deleted = 0) AS hasGoogleSubtasks - """) - abstract suspend fun getSubtaskInfo(): SubtaskInfo + suspend fun getSubtaskInfo(): SubtaskInfo = taskDao.getSubtaskInfo() - @RawQuery(observedEntities = [Place::class]) - abstract fun getTaskFactory(query: SimpleSQLiteQuery): DataSource.Factory + fun getTaskFactory(query: SimpleSQLiteQuery): DataSource.Factory = + taskDao.getTaskFactory(query) suspend fun touch(id: Long) = touch(listOf(id)) suspend fun touch(ids: List) { - ids.eachChunk { touchInternal(it) } + ids.eachChunk { taskDao.touch(ids) } workManager.sync(false) } - @Query("UPDATE tasks SET modified = :now WHERE _id in (:ids)") - internal abstract suspend fun touchInternal(ids: List, now: Long = currentTimeMillis()) - suspend fun setParent(parent: Long, tasks: List) = tasks.eachChunk { setParentInternal(parent, it) } - @Query("UPDATE tasks SET parent = :parent WHERE _id IN (:children)") - internal abstract suspend fun setParentInternal(parent: Long, children: List) + private suspend fun setParentInternal(parent: Long, children: List) = + taskDao.setParentInternal(parent, children) - @Transaction - open suspend fun fetchChildren(id: Long): List { - return fetch(getChildren(id)) - } + suspend fun fetchChildren(id: Long): List = taskDao.fetchChildren(id) - suspend fun getChildren(id: Long): List { - return getChildren(listOf(id)) - } + suspend fun getChildren(id: Long): List = taskDao.getChildren(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("UPDATE tasks SET collapsed = :collapsed WHERE _id = :id") - abstract suspend fun setCollapsed(id: Long, collapsed: Boolean) - - @Transaction - open suspend fun setCollapsed(preferences: Preferences, filter: Filter, collapsed: Boolean) { - fetchTasks(preferences, filter) - .filter(TaskContainer::hasChildren) - .map(TaskContainer::getId) - .eachChunk { collapse(it, collapsed) } - } + suspend fun getChildren(ids: List): List = taskDao.getChildren(ids) - @Query("UPDATE tasks SET collapsed = :collapsed WHERE _id IN (:ids)") - internal abstract suspend fun collapse(ids: List, collapsed: Boolean) + suspend fun setCollapsed(id: Long, collapsed: Boolean) = taskDao.setCollapsed(id, collapsed) + + suspend fun setCollapsed(preferences: Preferences, filter: Filter, collapsed: Boolean) = + taskDao.setCollapsed(preferences, filter, collapsed) // --- save // TODO: get rid of this super-hack @@ -226,49 +129,20 @@ SELECT EXISTS(SELECT 1 FROM tasks WHERE parent > 0 AND deleted = 0) AS hasSubtas } } - @Insert - abstract suspend fun insert(task: Task): Long + suspend fun insert(task: Task): Long = taskDao.insert(task) - @Update - abstract suspend fun update(task: Task): Int + suspend fun update(task: Task): Int = taskDao.update(task) - suspend fun createNew(task: Task) { - task.id = NO_ID - if (task.creationDate == 0L) { - task.creationDate = DateUtilities.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 - } + suspend fun createNew(task: Task) = taskDao.createNew(task) - suspend fun count(filter: Filter): Int { - val query = getQuery(filter.sqlQuery, Field.COUNT) - val start = if (BuildConfig.DEBUG) DateUtilities.now() else 0 - val count = count(query) - Timber.v("%sms: %s", DateUtilities.now() - start, query.sql) - return count - } + suspend fun count(filter: Filter): Int = taskDao.count(filter) - suspend fun fetchFiltered(filter: Filter): List { - return fetchFiltered(filter.getSqlQuery()) - } + suspend fun fetchFiltered(filter: Filter): List = taskDao.fetchFiltered(filter) - suspend fun fetchFiltered(queryTemplate: String): List { - val query = getQuery(queryTemplate, Task.FIELDS) - val start = if (BuildConfig.DEBUG) DateUtilities.now() else 0 - val tasks = fetchTasks(query) - Timber.v("%sms: %s", DateUtilities.now() - start, query.sql) - return tasks.map(TaskContainer::getTask) - } + suspend fun fetchFiltered(queryTemplate: String): List = + taskDao.fetchFiltered(queryTemplate) - @Query("SELECT _id FROM tasks LEFT JOIN google_tasks ON _id = gt_task AND gt_deleted = 0 LEFT JOIN caldav_tasks ON _id = cd_task AND cd_deleted = 0 WHERE gt_id IS NULL AND cd_id IS NULL AND parent = 0") - abstract suspend fun getLocalTasks(): List + suspend fun getLocalTasks(): List = taskDao.getLocalTasks() /** Generates SQL clauses */ object TaskCriteria { @@ -281,15 +155,4 @@ SELECT EXISTS(SELECT 1 FROM tasks WHERE parent > 0 AND deleted = 0) AS hasSubtas Task.HIDE_UNTIL.lte(Functions.now())) } } - - companion object { - const val TRANS_SUPPRESS_REFRESH = "suppress-refresh" - fun getQuery(queryTemplate: String, vararg fields: Field): SimpleSQLiteQuery { - return SimpleSQLiteQuery( - com.todoroo.andlib.sql.Query.select(*fields) - .withQueryTemplate(PermaSql.replacePlaceholdersForQuery(queryTemplate)) - .from(Task.TABLE) - .toString()) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/todoroo/astrid/data/Task.kt b/app/src/main/java/com/todoroo/astrid/data/Task.kt index ccdf5cfcb..d317f82e3 100644 --- a/app/src/main/java/com/todoroo/astrid/data/Task.kt +++ b/app/src/main/java/com/todoroo/astrid/data/Task.kt @@ -376,7 +376,7 @@ class Task : Parcelable { @Synchronized fun suppressRefresh() { - putTransitory(TaskDao.TRANS_SUPPRESS_REFRESH, true) + putTransitory(TRANS_SUPPRESS_REFRESH, true) } @Synchronized @@ -560,6 +560,8 @@ class Task : Parcelable { const val URGENCY_NEXT_WEEK = 4 const val URGENCY_IN_TWO_WEEKS = 5 + const val TRANS_SUPPRESS_REFRESH = "suppress-refresh" + /** * Creates due date for this task. If this due date has no time associated, we move it to the last * millisecond of the day. diff --git a/app/src/main/java/org/tasks/data/TaskDao.kt b/app/src/main/java/org/tasks/data/TaskDao.kt new file mode 100644 index 000000000..650b550a7 --- /dev/null +++ b/app/src/main/java/org/tasks/data/TaskDao.kt @@ -0,0 +1,252 @@ +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 +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.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 = DateUtilities.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 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 " + "WHERE remoteId = :remoteId") + abstract suspend fun setCompletionDate(remoteId: String, completionDate: Long) + + @Query("UPDATE tasks SET snoozeTime = :millis WHERE _id in (:taskIds)") + abstract suspend fun snooze(taskIds: List, millis: Long) + + @Query("SELECT tasks.* FROM tasks " + + "LEFT JOIN google_tasks ON tasks._id = google_tasks.gt_task " + + "WHERE gt_list_id IN (SELECT gtl_remote_id FROM google_task_lists WHERE gtl_account = :account)" + + "AND (tasks.modified > google_tasks.gt_last_sync OR google_tasks.gt_remote_id = '' OR google_tasks.gt_deleted > 0) " + + "ORDER BY CASE WHEN gt_parent = 0 THEN 0 ELSE 1 END, gt_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 (tasks.modified > caldav_tasks.cd_last_sync OR caldav_tasks.cd_last_sync = 0)""") + abstract suspend fun getCaldavTasksToPush(calendar: String): List + + @Query("SELECT * FROM TASKS " + + "WHERE completed = 0 AND deleted = 0 AND (notificationFlags > 0 OR notifications > 0)") + abstract suspend fun getTasksWithReminders(): 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 + + @Transaction + open suspend fun fetchTasks(callback: suspend (SubtaskInfo) -> List): List { + return fetchTasks(callback, getSubtaskInfo()) + } + + @Transaction + open suspend fun fetchTasks(callback: suspend (SubtaskInfo) -> List, subtasks: SubtaskInfo): List { + val start = if (BuildConfig.DEBUG) DateUtilities.now() else 0 + val queries = callback.invoke(subtasks) + val db = database.openHelper.writableDatabase + val last = queries.size - 1 + for (i in 0 until last) { + db.execSQL(queries[i]) + } + val result = fetchTasks(SimpleSQLiteQuery(queries[last])) + Timber.v("%sms: %s", DateUtilities.now() - start, queries.joinToString(";\n")) + return result + } + + suspend fun fetchTasks(preferences: Preferences, filter: Filter): List { + return fetchTasks { + TaskListQuery.getQuery(preferences, filter, it) + } + } + + @RawQuery + 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, + EXISTS(SELECT 1 + FROM google_tasks + INNER JOIN tasks ON gt_task = _id + WHERE deleted = 0 + AND gt_parent > 0 + AND gt_deleted = 0) AS hasGoogleSubtasks + """) + abstract suspend fun getSubtaskInfo(): SubtaskInfo + + @RawQuery(observedEntities = [Place::class]) + abstract fun getTaskFactory(query: SimpleSQLiteQuery): DataSource.Factory + + @Query("UPDATE tasks SET modified = :now WHERE _id in (:ids)") + abstract suspend fun touch(ids: List, now: Long = currentTimeMillis()) + + suspend fun setParent(parent: Long, tasks: List) = + tasks.eachChunk { setParentInternal(parent, it) } + + @Query("UPDATE tasks SET parent = :parent WHERE _id IN (:children)") + internal abstract suspend fun setParentInternal(parent: Long, children: List) + + @Transaction + open suspend fun fetchChildren(id: Long): List { + return fetch(getChildren(id)) + } + + suspend fun getChildren(id: Long): List { + return 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("UPDATE tasks SET collapsed = :collapsed WHERE _id = :id") + abstract suspend fun setCollapsed(id: Long, collapsed: Boolean) + + @Transaction + open suspend fun setCollapsed(preferences: Preferences, filter: Filter, collapsed: Boolean) { + fetchTasks(preferences, filter) + .filter(TaskContainer::hasChildren) + .map(TaskContainer::getId) + .eachChunk { collapse(it, collapsed) } + } + + @Query("UPDATE tasks SET collapsed = :collapsed WHERE _id IN (:ids)") + internal abstract suspend fun collapse(ids: List, collapsed: Boolean) + + @Insert + abstract suspend fun insert(task: Task): Long + + @Update + abstract suspend fun update(task: Task): Int + + suspend fun createNew(task: Task) { + task.id = NO_ID + if (task.creationDate == 0L) { + task.creationDate = DateUtilities.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 + } + + suspend fun count(filter: Filter): Int { + val query = getQuery(filter.sqlQuery, Field.COUNT) + val start = if (BuildConfig.DEBUG) DateUtilities.now() else 0 + val count = count(query) + Timber.v("%sms: %s", DateUtilities.now() - start, query.sql) + return count + } + + suspend fun fetchFiltered(filter: Filter): List { + return fetchFiltered(filter.getSqlQuery()) + } + + suspend fun fetchFiltered(queryTemplate: String): List { + val query = getQuery(queryTemplate, Task.FIELDS) + val start = if (BuildConfig.DEBUG) DateUtilities.now() else 0 + val tasks = fetchTasks(query) + Timber.v("%sms: %s", DateUtilities.now() - start, query.sql) + return tasks.map(TaskContainer::getTask) + } + + @Query("SELECT _id FROM tasks LEFT JOIN google_tasks ON _id = gt_task AND gt_deleted = 0 LEFT JOIN caldav_tasks ON _id = cd_task AND cd_deleted = 0 WHERE gt_id IS NULL AND 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 { + return 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 { + return SimpleSQLiteQuery( + com.todoroo.andlib.sql.Query.select(*fields) + .withQueryTemplate(PermaSql.replacePlaceholdersForQuery(queryTemplate)) + .from(Task.TABLE) + .toString()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/injection/ApplicationModule.kt b/app/src/main/java/org/tasks/injection/ApplicationModule.kt index 4b93d92ab..7c084f072 100644 --- a/app/src/main/java/org/tasks/injection/ApplicationModule.kt +++ b/app/src/main/java/org/tasks/injection/ApplicationModule.kt @@ -2,7 +2,6 @@ package org.tasks.injection import android.content.Context import com.todoroo.astrid.dao.Database -import com.todoroo.astrid.dao.TaskDao import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -13,7 +12,6 @@ import org.tasks.billing.BillingClient import org.tasks.billing.BillingClientImpl import org.tasks.billing.Inventory import org.tasks.data.* -import org.tasks.jobs.WorkManager import org.tasks.locale.Locale import org.tasks.location.Geocoder import org.tasks.location.MapboxGeocoder @@ -81,11 +79,7 @@ class ApplicationModule { @Provides @Singleton - fun getTaskDao(db: Database, workManager: WorkManager): TaskDao { - val taskDao = db.taskDao - taskDao.initialize(workManager) - return taskDao - } + fun getTaskDao(db: Database): TaskDao = db.taskDao @Provides @Singleton diff --git a/app/src/main/java/org/tasks/jobs/AfterSaveWork.kt b/app/src/main/java/org/tasks/jobs/AfterSaveWork.kt index dc52c442b..ee975308e 100644 --- a/app/src/main/java/org/tasks/jobs/AfterSaveWork.kt +++ b/app/src/main/java/org/tasks/jobs/AfterSaveWork.kt @@ -114,7 +114,7 @@ class AfterSaveWork @WorkerInject constructor( .putBoolean(EXTRA_PUSH_GTASKS, !suppress && !current.googleTaskUpToDate(original)) .putBoolean( EXTRA_PUSH_CALDAV, !suppress && (!current.caldavUpToDate(original) || forceCaldav)) - .putBoolean(EXTRA_SUPPRESS_REFRESH, current.checkTransitory(TaskDao.TRANS_SUPPRESS_REFRESH)) + .putBoolean(EXTRA_SUPPRESS_REFRESH, current.checkTransitory(Task.TRANS_SUPPRESS_REFRESH)) if (original != null) { builder .putLong(EXTRA_ORIG_COMPLETED, original.completionDate)