diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt index 826d40f6b..a7979698c 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt @@ -103,7 +103,6 @@ import org.tasks.data.entity.Task import org.tasks.data.listSettingsClass import org.tasks.data.open import org.tasks.data.sql.QueryTemplate -import org.tasks.data.withTransaction import org.tasks.databinding.FragmentTaskListBinding import org.tasks.dialogs.DateTimePicker.Companion.newDateTimePicker import org.tasks.dialogs.DialogBuilder @@ -1058,10 +1057,7 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL (intent.getSerializableExtra(EXTRAS_TASK_ID) as? ArrayList) ?.let { Timber.d("Repeating tasks: $it") - // hack to wait for task save transaction to complete - database.withTransaction { - taskDao.fetch(it) - } + taskDao.fetch(it) } ?.filterNot { it.readOnly } ?.takeIf { it.isNotEmpty() } 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 fedf2ca02..9ed789fce 100644 --- a/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt +++ b/app/src/main/java/com/todoroo/astrid/dao/TaskDao.kt @@ -98,22 +98,19 @@ class TaskDao @Inject constructor( */ suspend fun save(task: Task) = save(task, fetch(task.id)) - suspend fun save(tasks: List, originals: List) { - Timber.d("Saving $tasks") - taskDao.updateInternal(tasks) - tasks.forEach { task -> afterUpdate(task, originals.find { it.id == task.id }) } - } - suspend fun save(task: Task, original: Task?) { if (taskDao.update(task, original)) { Timber.d("Saved $task") afterUpdate(task, original) + if (!task.isSuppressRefresh()) { + localBroadcastManager.broadcastRefresh() + } workManager.triggerNotifications() workManager.scheduleRefresh() } } - private suspend fun afterUpdate(task: Task, original: Task?) { + suspend fun afterUpdate(task: Task, original: Task?) { val completionDateModified = task.completionDate != (original?.completionDate ?: 0) val deletionDateModified = task.deletionDate != (original?.deletionDate ?: 0) val justCompleted = completionDateModified && task.isCompleted @@ -131,9 +128,6 @@ class TaskDao @Inject constructor( if (completionDateModified || deletionDateModified) { geofenceApi.update(task.id) } - if (!task.isSuppressRefresh()) { - localBroadcastManager.broadcastRefresh() - } syncAdapters.sync(task, original) } diff --git a/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.kt b/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.kt index 407a4babd..a1f71f166 100644 --- a/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.kt +++ b/app/src/main/java/com/todoroo/astrid/repeats/RepeatTaskHelper.kt @@ -11,7 +11,6 @@ import com.todoroo.astrid.gcal.GCalHelper import net.fortuna.ical4j.model.Date import net.fortuna.ical4j.model.Recur import net.fortuna.ical4j.model.WeekDay -import org.tasks.LocalBroadcastManager import org.tasks.data.createDueDate import org.tasks.data.entity.Alarm import org.tasks.data.entity.Alarm.Companion.TYPE_SNOOZE @@ -33,12 +32,11 @@ class RepeatTaskHelper @Inject constructor( private val gcalHelper: GCalHelper, private val alarmService: AlarmService, private val taskDao: TaskDao, - private val localBroadcastManager: LocalBroadcastManager, ) { - suspend fun handleRepeat(task: Task) { + suspend fun handleRepeat(task: Task): Boolean { val recurrence = task.recurrence if (recurrence.isNullOrBlank()) { - return + return false } val repeatAfterCompletion = task.repeatFrom == RepeatFrom.COMPLETION_DATE val newDueDate: Long @@ -48,17 +46,15 @@ class RepeatTaskHelper @Inject constructor( rrule = initRRule(recurrence) count = rrule.count if (count == 1) { - broadcastCompletion(task) - return + return true } newDueDate = computeNextDueDate(task, recurrence, repeatAfterCompletion) if (newDueDate == -1L) { - broadcastCompletion(task) - return + return true } } catch (e: ParseException) { Timber.e(e) - return + return false } if (count > 1) { rrule.count = count - 1 @@ -70,18 +66,9 @@ class RepeatTaskHelper @Inject constructor( task.setDueDateAdjustingHideUntil(newDueDate) gcalHelper.rescheduleRepeatingTask(task) taskDao.save(task) - val previousDueDate = - oldDueDate - .takeIf { it > 0 } - ?: (newDueDate - (computeNextDueDate(task, recurrence, repeatAfterCompletion) - newDueDate)) + val previousDueDate = oldDueDate.takeIf { it > 0 } ?: computePreviousDueDate(task) rescheduleAlarms(task.id, previousDueDate, newDueDate) - broadcastCompletion(task, previousDueDate) - } - - private fun broadcastCompletion(task: Task, oldDueDate: Long = 0L) { - if (!task.isSuppressRefresh()) { - localBroadcastManager.broadcastTaskCompleted(task.id, oldDueDate) - } + return true } suspend fun undoRepeat(task: Task, oldDueDate: Long) { @@ -131,6 +118,9 @@ class RepeatTaskHelper @Inject constructor( companion object { private val weekdayCompare = Comparator { object1: WeekDay, object2: WeekDay -> WeekDay.getCalendarDay(object1) - WeekDay.getCalendarDay(object2) } + fun computePreviousDueDate(task: Task): Long = + task.dueDate - (computeNextDueDate(task, task.recurrence!!, task.repeatFrom == RepeatFrom.COMPLETION_DATE) - task.dueDate) + /** Compute next due date */ @Throws(ParseException::class) fun computeNextDueDate(task: Task, recurrence: String, repeatAfterCompletion: Boolean): Long { diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.kt b/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.kt index 0ffd14aab..7b2fed43b 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskCompleter.kt @@ -8,13 +8,12 @@ import android.media.RingtoneManager import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.gcal.GCalHelper import com.todoroo.astrid.repeats.RepeatTaskHelper +import com.todoroo.astrid.repeats.RepeatTaskHelper.Companion.computePreviousDueDate import dagger.hilt.android.qualifiers.ApplicationContext import org.tasks.LocalBroadcastManager -import org.tasks.data.dao.AlarmDao import org.tasks.data.dao.CaldavDao -import org.tasks.data.db.Database +import org.tasks.data.dao.CompletionDao import org.tasks.data.entity.Task -import org.tasks.data.withTransaction import org.tasks.jobs.WorkManager import org.tasks.notifications.NotificationManager import org.tasks.preferences.Preferences @@ -24,7 +23,6 @@ import javax.inject.Inject class TaskCompleter @Inject internal constructor( @ApplicationContext private val context: Context, - private val database: Database, private val taskDao: TaskDao, private val preferences: Preferences, private val notificationManager: NotificationManager, @@ -33,7 +31,7 @@ class TaskCompleter @Inject internal constructor( private val caldavDao: CaldavDao, private val gCalHelper: GCalHelper, private val workManager: WorkManager, - private val alarmDao: AlarmDao, + private val completionDao: CompletionDao, ) { suspend fun setComplete(taskId: Long, completed: Boolean = true) = taskDao @@ -56,10 +54,10 @@ class TaskCompleter @Inject internal constructor( .filterNotNull() .filter { it.isCompleted != completionDate > 0 } .filterNot { it.readOnly } - .let { - setComplete(it, completionDate) + .let { tasks -> + setComplete(tasks, completionDate) if (completed && !item.isRecurring) { - localBroadcastManager.broadcastTaskCompleted(ArrayList(it.map(Task::id))) + localBroadcastManager.broadcastTaskCompleted(tasks.map { it.id }) } } } @@ -70,27 +68,24 @@ class TaskCompleter @Inject internal constructor( } tasks.forEach { notificationManager.cancel(it.id) } val completed = completionDate > 0 - val modified = currentTimeMillis() + val repeated = ArrayList() Timber.d("Completing $tasks") - database.withTransaction { - alarmDao.deleteSnoozed(tasks.map { it.id }) - tasks - .map { - it.copy( - completionDate = completionDate, - modificationDate = modified, - ) + completionDao.complete( + tasks = tasks, + completionDate = completionDate, + afterSave = { updated -> + updated.forEach { saved -> + val original = tasks.find { it.id == saved.id } + taskDao.afterUpdate(saved, original) } - .also { completed -> - completed.subList(0, completed.lastIndex).forEach { it.suppressRefresh() } - taskDao.save(completed, tasks) - } - .forEach { task -> + updated.forEach { task -> if (completed && task.isRecurring) { gCalHelper.updateEvent(task) if (caldavDao.getAccountForTask(task.id)?.isSuppressRepeatingTasks != true) { - repeatTaskHelper.handleRepeat(task) + if (repeatTaskHelper.handleRepeat(task)) { + repeated.add(task) + } if (task.completionDate == 0L) { // un-complete children setComplete(task, false) @@ -98,9 +93,16 @@ class TaskCompleter @Inject internal constructor( } } } - } + } + ) + localBroadcastManager.broadcastRefresh() workManager.triggerNotifications() workManager.scheduleRefresh() + repeated.lastOrNull()?.let { task -> + val oldDueDate = tasks.find { it.id == task.id }?.dueDate?.takeIf { it > 0 } + ?: computePreviousDueDate(task) + localBroadcastManager.broadcastTaskCompleted(arrayListOf(task.id), oldDueDate) + } if (completed && notificationManager.currentInterruptionFilter == INTERRUPTION_FILTER_ALL) { preferences .completionSound diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt index 003719252..119fbbe22 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt @@ -10,23 +10,19 @@ import org.tasks.data.dao.DeletionDao import org.tasks.data.dao.LocationDao import org.tasks.data.dao.TaskDao import org.tasks.data.dao.UserActivityDao -import org.tasks.data.db.Database import org.tasks.data.db.SuspendDbUtils.chunkedMap import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.Task import org.tasks.data.pictureUri -import org.tasks.data.withTransaction import org.tasks.files.FileHelper import org.tasks.location.GeofenceApi import org.tasks.notifications.NotificationManager import org.tasks.sync.SyncAdapters -import timber.log.Timber import javax.inject.Inject class TaskDeleter @Inject constructor( @ApplicationContext private val context: Context, - private val database: Database, private val deletionDao: DeletionDao, private val taskDao: TaskDao, private val localBroadcastManager: LocalBroadcastManager, @@ -47,11 +43,10 @@ class TaskDeleter @Inject constructor( .let { taskDao.fetch(it.toList()) } .filterNot { it.readOnly } .map { it.id } - Timber.d("markDeleted $ids") - database.withTransaction { - deletionDao.markDeleted(ids) - cleanup(ids) - } + deletionDao.markDeleted( + ids = ids, + cleanup = { cleanup(it) } + ) syncAdapters.sync() localBroadcastManager.broadcastRefresh() taskDao.fetch(ids) @@ -62,31 +57,28 @@ class TaskDeleter @Inject constructor( suspend fun delete(task: Long) = delete(listOf(task)) suspend fun delete(tasks: List) { - Timber.d("Deleting $tasks") - database.withTransaction { - deletionDao.delete(tasks) - cleanup(tasks) - } + deletionDao.delete( + ids = tasks, + cleanup = { cleanup(it) } + ) localBroadcastManager.broadcastRefresh() } suspend fun delete(list: CaldavCalendar) { vtodoCache.delete(list) - Timber.d("Deleting $list") - database.withTransaction { - val tasks = deletionDao.delete(list) - delete(tasks) - } + deletionDao.delete( + caldavCalendar = list, + cleanup = { cleanup(it) } + ) localBroadcastManager.broadcastRefreshList() } suspend fun delete(account: CaldavAccount) { vtodoCache.delete(account) - Timber.d("Deleting $account") - database.withTransaction { - val tasks = deletionDao.delete(account) - delete(tasks) - } + deletionDao.delete( + caldavAccount = account, + cleanup = { cleanup(it) } + ) localBroadcastManager.broadcastRefreshList() } diff --git a/app/src/main/java/org/tasks/LocalBroadcastManager.kt b/app/src/main/java/org/tasks/LocalBroadcastManager.kt index 8eef00712..bb43b1840 100644 --- a/app/src/main/java/org/tasks/LocalBroadcastManager.kt +++ b/app/src/main/java/org/tasks/LocalBroadcastManager.kt @@ -5,7 +5,6 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import androidx.localbroadcastmanager.content.LocalBroadcastManager -import com.google.common.collect.Lists import com.todoroo.astrid.api.AstridApiConstants import dagger.hilt.android.qualifiers.ApplicationContext import org.tasks.widget.AppWidgetManager @@ -56,17 +55,9 @@ class LocalBroadcastManager @Inject constructor( localBroadcastManager.sendBroadcast(Intent(REFRESH_PREFERENCES)) } - fun broadcastTaskCompleted(id: Long, oldDueDate: Long) { - broadcastTaskCompleted(Lists.newArrayList(id), oldDueDate) - } - - fun broadcastTaskCompleted(id: ArrayList) { - broadcastTaskCompleted(id, 0) - } - - private fun broadcastTaskCompleted(id: ArrayList, oldDueDate: Long) { + fun broadcastTaskCompleted(id: List, oldDueDate: Long = 0L) { val intent = Intent(TASK_COMPLETED) - intent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, id) + intent.putExtra(AstridApiConstants.EXTRAS_TASK_ID, ArrayList(id)) intent.putExtra(AstridApiConstants.EXTRAS_OLD_DUE_DATE, oldDueDate) localBroadcastManager.sendBroadcast(intent) } diff --git a/app/src/main/java/org/tasks/injection/ApplicationModule.kt b/app/src/main/java/org/tasks/injection/ApplicationModule.kt index bd6ace2cf..5e886c3a5 100644 --- a/app/src/main/java/org/tasks/injection/ApplicationModule.kt +++ b/app/src/main/java/org/tasks/injection/ApplicationModule.kt @@ -15,6 +15,7 @@ import org.tasks.analytics.Firebase import org.tasks.billing.BillingClient import org.tasks.billing.BillingClientImpl import org.tasks.billing.Inventory +import org.tasks.compose.drawer.DrawerConfiguration import org.tasks.data.dao.AlarmDao import org.tasks.data.dao.Astrid2ContentProviderDao import org.tasks.data.dao.CaldavDao @@ -30,13 +31,12 @@ import org.tasks.data.dao.TaskDao import org.tasks.data.dao.TaskListMetadataDao import org.tasks.data.dao.UserActivityDao import org.tasks.data.db.Database +import org.tasks.filters.FilterProvider import org.tasks.filters.PreferenceDrawerConfiguration import org.tasks.jobs.WorkManager import org.tasks.kmp.createDataStore -import org.tasks.compose.drawer.DrawerConfiguration -import org.tasks.filters.FilterProvider -import org.tasks.preferences.TasksPreferences import org.tasks.preferences.Preferences +import org.tasks.preferences.TasksPreferences import java.util.Locale import javax.inject.Singleton @@ -117,6 +117,10 @@ class ApplicationModule { @Singleton fun getPrincipalDao(db: Database) = db.principalDao() + @Provides + @Singleton + fun getCompletionDao(db: Database) = db.completionDao() + @Provides fun getBillingClient( @ApplicationContext context: Context, diff --git a/data/src/commonMain/kotlin/org/tasks/data/RoomDatabaseExtensions.kt b/data/src/commonMain/kotlin/org/tasks/data/RoomDatabaseExtensions.kt deleted file mode 100644 index e5dbc639b..000000000 --- a/data/src/commonMain/kotlin/org/tasks/data/RoomDatabaseExtensions.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.tasks.data - -import androidx.room.RoomDatabase -import androidx.room.TransactionScope -import androidx.room.Transactor -import androidx.room.useWriterConnection - -suspend fun RoomDatabase.withTransaction(block: suspend TransactionScope.() -> T): T = - useWriterConnection { transactor -> - transactor.withTransaction(Transactor.SQLiteTransactionType.IMMEDIATE) { - block() - } - } diff --git a/data/src/commonMain/kotlin/org/tasks/data/dao/CaldavDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/CaldavDao.kt index f94b7fba8..ff844c7bd 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/CaldavDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/CaldavDao.kt @@ -5,6 +5,7 @@ import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction import androidx.room.Update import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow @@ -12,7 +13,6 @@ import org.tasks.data.CaldavFilters import org.tasks.data.CaldavTaskContainer import org.tasks.data.NO_ORDER import org.tasks.data.TaskContainer -import org.tasks.data.db.Database import org.tasks.data.db.DbUtils.dbchunk import org.tasks.data.db.SuspendDbUtils.chunkedMap import org.tasks.data.entity.CaldavAccount @@ -22,13 +22,12 @@ import org.tasks.data.entity.CaldavAccount.Companion.TYPE_TASKS import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.Task -import org.tasks.data.withTransaction import org.tasks.time.DateTimeUtils2.currentTimeMillis const val APPLE_EPOCH = 978307200000L // 1/1/2001 GMT @Dao -abstract class CaldavDao(private val database: Database) { +abstract class CaldavDao { @Query("SELECT COUNT(*) FROM caldav_lists WHERE cdl_account = :account") abstract suspend fun listCount(account: String): Int @@ -104,23 +103,21 @@ ORDER BY CASE cda_account_type suspend fun insert(task: Task, caldavTask: CaldavTask, addToTop: Boolean): Long { Logger.d("CaldavDao") { "insert task=$task caldavTask=$caldavTask addToTop=$addToTop)" } - return database.withTransaction { - if (task.order != null) { - return@withTransaction 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) - id + 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(""" @@ -309,51 +306,49 @@ GROUP BY caldav_lists.cdl_uuid + "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) - suspend fun move( + @Transaction + open suspend fun move( task: TaskContainer, previousParent: Long, newParent: Long, newPosition: Long?, ) { Logger.d("CaldavDao") { "move task=$task previousParent=$previousParent newParent=$newParent newPosition=$newPosition" } - database.withTransaction { - 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) - } + 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) } + task.task.order = newPosition + setTaskOrder(task.id, newPosition) } - suspend fun shiftDown(calendar: String, parent: Long, from: Long, to: Long? = null) { + @Transaction + open suspend fun shiftDown(calendar: String, parent: Long, from: Long, to: Long? = null) { Logger.d("CaldavDao") { "shiftDown calendar=$calendar parent=$parent from=$from to=$to" } - database.withTransaction { - val updated = ArrayList() - 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 - } + val updated = ArrayList() + 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) } } + updateTasks(updated) + updated + .map(Task::id) + .dbchunk() + .forEach { touchInternal(it) } } @Query("UPDATE tasks SET modified = :modificationTime WHERE _id in (:ids)") diff --git a/data/src/commonMain/kotlin/org/tasks/data/dao/CompletionDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/CompletionDao.kt new file mode 100644 index 000000000..12a2234da --- /dev/null +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/CompletionDao.kt @@ -0,0 +1,31 @@ +package org.tasks.data.dao + +import androidx.room.Dao +import androidx.room.Transaction +import co.touchlab.kermit.Logger +import org.tasks.data.db.Database +import org.tasks.data.entity.Task +import org.tasks.time.DateTimeUtils2.currentTimeMillis + +@Dao +abstract class CompletionDao(private val db: Database) { + @Transaction + open suspend fun complete( + tasks: List, + completionDate: Long, + afterSave: suspend (List) -> Unit, + ) { + Logger.d("CompletionDao") { "complete tasks=$tasks completionDate=$completionDate" } + val modified = currentTimeMillis() + val updated = tasks + .map { + it.copy( + completionDate = completionDate, + modificationDate = modified, + ) + } + db.alarmDao().deleteSnoozed(tasks.map { it.id }) + db.taskDao().updateInternal(updated) + afterSave(updated) + } +} \ No newline at end of file diff --git a/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt index da6192312..103bede72 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt @@ -3,17 +3,16 @@ package org.tasks.data.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Query +import androidx.room.Transaction import co.touchlab.kermit.Logger import org.tasks.data.dao.CaldavDao.Companion.LOCAL -import org.tasks.data.db.Database import org.tasks.data.db.SuspendDbUtils.chunkedMap import org.tasks.data.db.SuspendDbUtils.eachChunk import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavCalendar -import org.tasks.data.withTransaction @Dao -abstract class DeletionDao(private val database: Database) { +abstract class DeletionDao { @Query("DELETE FROM tasks WHERE _id IN(:ids)") internal abstract suspend fun deleteTasks(ids: List) @@ -43,15 +42,29 @@ WHERE recurring = 1 """) abstract suspend fun internalHasRecurringAncestors(ids: List): List - suspend fun delete(ids: List) { ids.eachChunk { deleteTasks(it) } } + @Transaction + open suspend fun delete( + ids: List, + cleanup: suspend (List) -> Unit, + ) { + Logger.d("DeletionDao") { "delete ids=$ids" } + ids.eachChunk { deleteTasks(it) } + cleanup(ids) + } @Query("UPDATE tasks " + "SET modified = (strftime('%s','now')*1000), deleted = (strftime('%s','now')*1000)" + "WHERE _id IN(:ids)") internal abstract suspend fun markDeletedInternal(ids: List) - suspend fun markDeleted(ids: Iterable) { + @Transaction + open suspend fun markDeleted( + ids: Iterable, + cleanup: suspend (List) -> Unit, + ) { + Logger.d("DeletionDao") { "markDeleted ids=$ids" } ids.eachChunk(this::markDeletedInternal) + cleanup(ids.toList()) } @Query("SELECT cd_task FROM caldav_tasks WHERE cd_calendar = :calendar AND cd_deleted = 0") @@ -60,14 +73,15 @@ WHERE recurring = 1 @Delete internal abstract suspend fun deleteCaldavCalendar(caldavCalendar: CaldavCalendar) - suspend fun delete(caldavCalendar: CaldavCalendar): List { + @Transaction + open suspend fun delete( + caldavCalendar: CaldavCalendar, + cleanup: suspend (List) -> Unit, + ) { Logger.d("DeletionDao") { "deleting $caldavCalendar" } - return database.withTransaction { - val tasks = getActiveCaldavTasks(caldavCalendar.uuid!!) - delete(tasks) - deleteCaldavCalendar(caldavCalendar) - tasks - } + val tasks = getActiveCaldavTasks(caldavCalendar.uuid!!) + delete(tasks, cleanup) + deleteCaldavCalendar(caldavCalendar) } @Query("SELECT * FROM caldav_lists WHERE cdl_account = :account") @@ -79,15 +93,15 @@ WHERE recurring = 1 @Query("DELETE FROM tasks WHERE _id IN (SELECT _id FROM tasks INNER JOIN caldav_tasks ON _id = cd_task INNER JOIN caldav_lists ON cdl_uuid = cd_calendar WHERE cdl_account = '$LOCAL' AND deleted > 0 AND cd_deleted = 0)") abstract suspend fun purgeDeleted() - suspend fun delete(caldavAccount: CaldavAccount): List { + @Transaction + open suspend fun delete( + caldavAccount: CaldavAccount, + cleanup: suspend (List) -> Unit, + ) { Logger.d("DeletionDao") { "deleting $caldavAccount" } - return database.withTransaction { - val deleted = ArrayList() - for (calendar in getCalendars(caldavAccount.uuid!!)) { - deleted.addAll(delete(calendar)) - } - deleteCaldavAccount(caldavAccount) - deleted + for (calendar in getCalendars(caldavAccount.uuid!!)) { + delete(calendar, cleanup) } + deleteCaldavAccount(caldavAccount) } } \ No newline at end of file diff --git a/data/src/commonMain/kotlin/org/tasks/data/dao/GoogleTaskDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/GoogleTaskDao.kt index d581eec1a..e15f7c4d3 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/GoogleTaskDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/GoogleTaskDao.kt @@ -4,34 +4,32 @@ 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 co.touchlab.kermit.Logger -import org.tasks.data.db.Database import org.tasks.data.entity.CaldavAccount.Companion.TYPE_GOOGLE_TASKS import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.Task -import org.tasks.data.withTransaction @Dao -abstract class GoogleTaskDao(private val database: Database) { +abstract class GoogleTaskDao { @Insert abstract suspend fun insert(task: CaldavTask): Long @Insert abstract suspend fun insert(tasks: Iterable) - suspend fun insertAndShift(task: Task, caldavTask: CaldavTask, top: Boolean) { + @Transaction + open suspend fun insertAndShift(task: Task, caldavTask: CaldavTask, top: Boolean) { Logger.d("GoogleTaskDao") { "insertAndShift task=$task caldavTask=$caldavTask top=$top" } - database.withTransaction { - if (top) { - task.order = 0 - shiftDown(caldavTask.calendar!!, task.parent, 0) - } else { - task.order = getBottom(caldavTask.calendar!!, task.parent) - } - insert(caldavTask) - update(task) + if (top) { + task.order = 0 + shiftDown(caldavTask.calendar!!, task.parent, 0) + } else { + task.order = getBottom(caldavTask.calendar!!, task.parent) } + insert(caldavTask) + update(task) } @Query("UPDATE tasks SET `order` = `order` + 1 WHERE parent = :parent AND `order` >= :position AND _id IN (SELECT cd_task FROM caldav_tasks WHERE cd_calendar = :listId)") @@ -46,26 +44,25 @@ abstract class GoogleTaskDao(private val database: Database) { @Query("UPDATE tasks SET `order` = `order` - 1 WHERE parent = :parent AND `order` >= :position AND _id IN (SELECT cd_task FROM caldav_tasks WHERE cd_calendar = :listId)") internal abstract suspend fun shiftUp(listId: String, parent: Long, position: Long) - suspend fun move(task: Task, list: String, newParent: Long, newPosition: Long) { + @Transaction + open suspend fun move(task: Task, list: String, newParent: Long, newPosition: Long) { Logger.d("GoogleTaskDao") { "move task=$task list=$list newParent=$newParent newPosition=$newPosition" } - database.withTransaction { - val previousParent = task.parent - val previousPosition = task.order!! - if (newParent == previousParent) { - if (previousPosition < newPosition) { - shiftUp(list, newParent, previousPosition, newPosition) - } else { - shiftDown(list, newParent, previousPosition, newPosition) - } + val previousParent = task.parent + val previousPosition = task.order!! + if (newParent == previousParent) { + if (previousPosition < newPosition) { + shiftUp(list, newParent, previousPosition, newPosition) } else { - shiftUp(list, previousParent, previousPosition) - shiftDown(list, newParent, newPosition) + shiftDown(list, newParent, previousPosition, newPosition) } - task.parent = newParent - task.order = newPosition - update(task) - setMoved(task.id, list) + } else { + shiftUp(list, previousParent, previousPosition) + shiftDown(list, newParent, newPosition) } + task.parent = newParent + task.order = newPosition + update(task) + setMoved(task.id, list) } @Query("UPDATE caldav_tasks SET gt_moved = 1 WHERE cd_task = :task and cd_calendar = :list") @@ -170,26 +167,24 @@ WHERE cd_remote_id = :id suspend fun reposition(caldavDao: CaldavDao, listId: String) { Logger.d("GoogleTaskDao") { "reposition listId=$listId" } - database.withTransaction { - caldavDao.updateParents(listId) - val orderedTasks = getByRemoteOrder(listId) - var subtasks = 0L - var parent = 0L - for (task in orderedTasks) { - if (task.parent > 0) { - if (task.order != subtasks) { - task.order = subtasks - update(task) - } - subtasks++ - } else { - subtasks = 0 - if (task.order != parent) { - task.order = parent - update(task) - } - parent++ + caldavDao.updateParents(listId) + val orderedTasks = getByRemoteOrder(listId) + var subtasks = 0L + var parent = 0L + for (task in orderedTasks) { + if (task.parent > 0) { + if (task.order != subtasks) { + task.order = subtasks + update(task) } + subtasks++ + } else { + subtasks = 0 + if (task.order != parent) { + task.order = parent + update(task) + } + parent++ } } } diff --git a/data/src/commonMain/kotlin/org/tasks/data/dao/PrincipalDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/PrincipalDao.kt index 48eebe78e..cd06d944f 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/PrincipalDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/PrincipalDao.kt @@ -4,19 +4,17 @@ 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 co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import org.tasks.data.PrincipalWithAccess -import org.tasks.data.db.Database import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.Principal import org.tasks.data.entity.PrincipalAccess -import org.tasks.data.withTransaction @Dao -abstract class PrincipalDao(private val database: Database) { +abstract class PrincipalDao { @Insert abstract suspend fun insert(principal: Principal): Long @@ -36,15 +34,9 @@ WHERE list = :list @Delete abstract suspend fun delete(access: PrincipalAccess) - suspend fun getAll(): List { - Logger.d("PrincipalDao") { "getAll" } - return database.withTransaction { - getAllInternal() - } - } - + @Transaction @Query("SELECT * FROM principal_access") - internal abstract suspend fun getAllInternal(): List + abstract suspend fun getAll(): List suspend fun getOrCreatePrincipal(account: CaldavAccount, href: String, displayName: String? = null) = findPrincipal(account.id, href) diff --git a/data/src/commonMain/kotlin/org/tasks/data/dao/TagDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/TagDao.kt index 052d91199..445da71e0 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/TagDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/TagDao.kt @@ -4,15 +4,14 @@ import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query +import androidx.room.Transaction import co.touchlab.kermit.Logger -import org.tasks.data.db.Database import org.tasks.data.entity.Tag import org.tasks.data.entity.TagData import org.tasks.data.entity.Task -import org.tasks.data.withTransaction @Dao -abstract class TagDao(private val database: Database) { +abstract class TagDao { @Query("UPDATE tags SET name = :name WHERE tag_uid = :tagUid") abstract suspend fun rename(tagUid: String, name: String) @@ -37,17 +36,16 @@ abstract class TagDao(private val database: Database) { @Delete abstract suspend fun delete(tags: List) + @Transaction open suspend fun applyTags(task: Task, tagDataDao: TagDataDao, current: Collection) { Logger.d("TagDao") { "applyTags task=$task current=$current" } - database.withTransaction { - val taskId = task.id - val existing = HashSet(tagDataDao.getTagDataForTask(taskId)) - val selected = current.toMutableSet() - val added = selected subtract existing - val removed = existing subtract selected - deleteTags(taskId, removed.map { td -> td.remoteId!! }) - insert(task, added) - } + val taskId = task.id + val existing = HashSet(tagDataDao.getTagDataForTask(taskId)) + val selected = current.toMutableSet() + val added = selected subtract existing + val removed = existing subtract selected + deleteTags(taskId, removed.map { td -> td.remoteId!! }) + insert(task, added) } suspend fun insert(task: Task, tags: Collection) { diff --git a/data/src/commonMain/kotlin/org/tasks/data/dao/TagDataDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/TagDataDao.kt index 0436586a3..9a5804897 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/TagDataDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/TagDataDao.kt @@ -4,21 +4,20 @@ 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 co.touchlab.kermit.Logger import kotlinx.coroutines.flow.Flow import org.tasks.data.NO_ORDER import org.tasks.data.TagFilters -import org.tasks.data.db.Database import org.tasks.data.db.DbUtils import org.tasks.data.entity.Tag import org.tasks.data.entity.TagData import org.tasks.data.entity.Task -import org.tasks.data.withTransaction import org.tasks.time.DateTimeUtils2.currentTimeMillis @Dao -abstract class TagDataDao(private val database: Database) { +abstract class TagDataDao { @Query("SELECT * FROM tagdata") abstract fun subscribeToTags(): Flow> @@ -81,46 +80,44 @@ abstract class TagDataDao(private val database: Database) { + " GROUP BY tasks._id") internal abstract suspend fun getAllTags(tasks: List): List - suspend fun applyTags( + @Transaction + open suspend fun applyTags( tasks: List, partiallySelected: List, selected: List ): List { Logger.d("TagDataDao") { "applyTags tasks=$tasks partiallySelected=$partiallySelected selected=$selected" } - return database.withTransaction { - val modified = HashSet() - val keep = partiallySelected.plus(selected).map { it.remoteId!! } - for (sublist in tasks.chunked(DbUtils.MAX_SQLITE_ARGS - keep.size)) { - val tags = tagsToDelete(sublist.map(Task::id), keep) - deleteTags(tags) - modified.addAll(tags.map(Tag::task)) - } - for (task in tasks) { - val added = selected subtract getTagDataForTask(task.id) - if (added.isNotEmpty()) { - modified.add(task.id) - insert( - added.map { - Tag( - task = task.id, - taskUid = task.uuid, - name = it.name, - tagUid = it.remoteId - ) - } - ) - } + val modified = HashSet() + val keep = partiallySelected.plus(selected).map { it.remoteId!! } + for (sublist in tasks.chunked(DbUtils.MAX_SQLITE_ARGS - keep.size)) { + val tags = tagsToDelete(sublist.map(Task::id), keep) + deleteTags(tags) + modified.addAll(tags.map(Tag::task)) + } + for (task in tasks) { + val added = selected subtract getTagDataForTask(task.id) + if (added.isNotEmpty()) { + modified.add(task.id) + insert( + added.map { + Tag( + task = task.id, + taskUid = task.uuid, + name = it.name, + tagUid = it.remoteId + ) + } + ) } - ArrayList(modified) } + return ArrayList(modified) } - suspend fun delete(tagData: TagData) { + @Transaction + open suspend fun delete(tagData: TagData) { Logger.d("TagDataDao") { "deleting $tagData" } - database.withTransaction { - deleteTags(tagData.remoteId!!) - deleteTagData(tagData) - } + deleteTags(tagData.remoteId!!) + deleteTagData(tagData) } @Delete diff --git a/data/src/commonMain/kotlin/org/tasks/data/db/Database.kt b/data/src/commonMain/kotlin/org/tasks/data/db/Database.kt index 581f1d130..8637414da 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/db/Database.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/db/Database.kt @@ -6,6 +6,7 @@ import androidx.room.RoomDatabase import org.tasks.data.dao.AlarmDao import org.tasks.data.dao.Astrid2ContentProviderDao import org.tasks.data.dao.CaldavDao +import org.tasks.data.dao.CompletionDao import org.tasks.data.dao.DeletionDao import org.tasks.data.dao.FilterDao import org.tasks.data.dao.GoogleTaskDao @@ -80,6 +81,7 @@ abstract class Database : RoomDatabase() { abstract fun contentProviderDao(): Astrid2ContentProviderDao abstract fun upgraderDao(): UpgraderDao abstract fun principalDao(): PrincipalDao + abstract fun completionDao(): CompletionDao /** @return human-readable database name for debugging */