diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskCreator.kt b/app/src/main/java/com/todoroo/astrid/service/TaskCreator.kt index dc6a13202..e3fd919a0 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskCreator.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskCreator.kt @@ -193,7 +193,7 @@ class TaskCreator @Inject constructor( } catch (e: Throwable) { Timber.e(e) } - task.setTags(tags) + task.putTransitory(Tag.KEY, tags) return task } 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 4ecb15aae..b8faa5abd 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt @@ -17,6 +17,7 @@ import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.Task import org.tasks.data.inTransaction +import org.tasks.data.pictureUri import org.tasks.data.withTransaction import org.tasks.files.FileHelper import org.tasks.location.GeofenceApi diff --git a/app/src/main/java/com/todoroo/astrid/service/Upgrader.kt b/app/src/main/java/com/todoroo/astrid/service/Upgrader.kt index 8214ff153..7b945070b 100644 --- a/app/src/main/java/com/todoroo/astrid/service/Upgrader.kt +++ b/app/src/main/java/com/todoroo/astrid/service/Upgrader.kt @@ -20,6 +20,7 @@ import org.tasks.caldav.iCalendar.Companion.order import org.tasks.caldav.iCalendar.Companion.parent import org.tasks.data.CaldavTaskContainer import org.tasks.data.Location +import org.tasks.data.convertPictureUri import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.FilterDao import org.tasks.data.dao.GoogleTaskListDao diff --git a/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt b/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt index 4450cb10b..fe3532c88 100644 --- a/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt +++ b/app/src/main/java/org/tasks/backup/TasksJsonImporter.kt @@ -22,6 +22,7 @@ import kotlinx.serialization.json.jsonPrimitive import org.tasks.LocalBroadcastManager import org.tasks.R import org.tasks.caldav.VtodoCache +import org.tasks.data.convertPictureUri import org.tasks.data.dao.AlarmDao import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.FilterDao diff --git a/app/src/main/java/org/tasks/compose/edit/CommentsRow.kt b/app/src/main/java/org/tasks/compose/edit/CommentsRow.kt index 8e200470d..8ec63d96c 100644 --- a/app/src/main/java/org/tasks/compose/edit/CommentsRow.kt +++ b/app/src/main/java/org/tasks/compose/edit/CommentsRow.kt @@ -2,7 +2,11 @@ package org.tasks.compose.edit import android.net.Uri import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -15,7 +19,8 @@ import org.tasks.R import org.tasks.compose.DeleteButton import org.tasks.compose.TaskEditRow import org.tasks.data.entity.UserActivity -import java.util.* +import org.tasks.data.pictureUri +import java.util.Locale @Composable fun CommentsRow( diff --git a/app/src/main/java/org/tasks/data/UserActivityExtensions.kt b/app/src/main/java/org/tasks/data/UserActivityExtensions.kt new file mode 100644 index 000000000..d4bfdcae5 --- /dev/null +++ b/app/src/main/java/org/tasks/data/UserActivityExtensions.kt @@ -0,0 +1,41 @@ +package org.tasks.data + +import android.net.Uri +import org.json.JSONException +import org.json.JSONObject +import org.tasks.data.entity.UserActivity +import timber.log.Timber +import java.io.File + +fun UserActivity.setPicture(uri: Uri?) { + picture = uri?.toString() +} + +val UserActivity.pictureUri: Uri? + get() = if (picture.isNullOrBlank()) null else Uri.parse(picture) + +fun UserActivity.convertPictureUri() { + setPicture(getLegacyPictureUri(picture)) +} + +private fun getLegacyPictureUri(value: String?): Uri? { + return try { + if (value.isNullOrBlank()) { + return null + } + if (value.contains("uri") || value.contains("path")) { + val json = JSONObject(value) + if (json.has("uri")) { + return Uri.parse(json.getString("uri")) + } + if (json.has("path")) { + val path = json.getString("path") + return Uri.fromFile(File(path)) + } + } + null + } catch (e: JSONException) { + Timber.e(e) + null + } +} diff --git a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt index 98dce486a..5c6a95c23 100644 --- a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt @@ -57,6 +57,7 @@ import org.tasks.data.entity.Task.Companion.NOTIFY_MODE_NONSTOP import org.tasks.data.entity.Task.Companion.hasDueTime import org.tasks.data.entity.TaskAttachment import org.tasks.data.entity.UserActivity +import org.tasks.data.setPicture import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.files.FileHelper import org.tasks.location.GeofenceApi diff --git a/build.gradle.kts b/build.gradle.kts index 9883f2b55..dca1bac6e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.ksp).apply(false) alias(libs.plugins.androidLibrary).apply(false) alias(libs.plugins.kotlinMultiplatform).apply(false) + alias(libs.plugins.jetbrains.kotlin.jvm) apply false } buildscript { diff --git a/data/build.gradle.kts b/data/build.gradle.kts index e7082015f..ea9960a81 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -27,6 +27,7 @@ kotlin { } } } + jvm() sourceSets { commonMain.dependencies { implementation(libs.androidx.room) @@ -34,6 +35,7 @@ kotlin { implementation(libs.kermit) } } + task("testClasses") } android { namespace = "org.tasks.data" diff --git a/data/src/androidMain/kotlin/org/tasks/Platform.android.kt b/data/src/androidMain/kotlin/org/tasks/Platform.android.kt new file mode 100644 index 000000000..e0fd3f3c7 --- /dev/null +++ b/data/src/androidMain/kotlin/org/tasks/Platform.android.kt @@ -0,0 +1,13 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package org.tasks + +import android.os.Parcelable +import kotlinx.parcelize.RawValue +import org.tasks.data.BuildConfig + +actual typealias CommonParcelable = Parcelable + +actual typealias CommonRawValue = RawValue + +actual val IS_DEBUG = BuildConfig.DEBUG \ No newline at end of file diff --git a/data/src/androidMain/kotlin/org/tasks/Platform.kt b/data/src/androidMain/kotlin/org/tasks/Platform.kt deleted file mode 100644 index 984894ea7..000000000 --- a/data/src/androidMain/kotlin/org/tasks/Platform.kt +++ /dev/null @@ -1,7 +0,0 @@ -@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") - -package org.tasks - -import android.os.Parcelable - -actual typealias CommonParcelable = Parcelable \ No newline at end of file diff --git a/data/src/commonMain/kotlin/org/tasks/Platform.kt b/data/src/commonMain/kotlin/org/tasks/Platform.common.kt similarity index 51% rename from data/src/commonMain/kotlin/org/tasks/Platform.kt rename to data/src/commonMain/kotlin/org/tasks/Platform.common.kt index 244ee24e7..1be411992 100644 --- a/data/src/commonMain/kotlin/org/tasks/Platform.kt +++ b/data/src/commonMain/kotlin/org/tasks/Platform.common.kt @@ -6,4 +6,10 @@ package org.tasks @Retention(AnnotationRetention.BINARY) annotation class CommonParcelize -expect interface CommonParcelable \ No newline at end of file +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.BINARY) +expect annotation class CommonRawValue() + +expect interface CommonParcelable + +expect val IS_DEBUG: Boolean \ No newline at end of file diff --git a/data/src/commonMain/kotlin/org/tasks/data/SQLiteStatementExtensions.kt b/data/src/commonMain/kotlin/org/tasks/data/SQLiteStatementExtensions.kt index 0ed7d1f18..56cbe00f0 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/SQLiteStatementExtensions.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/SQLiteStatementExtensions.kt @@ -114,7 +114,7 @@ fun SQLiteStatement.getTasks(): List { remoteOrder = getLong(_cursorIndexOfRemoteOrder), ) } - val accountType = getInt(_cursorIndexOfAccountType) + val accountType = getIntOrNull(_cursorIndexOfAccountType) ?: 0 val geofence = getLongOrNull(_cursorIndexOfId_2)?.takeIf { it > 0 }?.let { Geofence( id = it, @@ -156,7 +156,7 @@ fun SQLiteStatement.getTasks(): List { children = getIntOrNull(_cursorIndexOfChildren) ?: 0, primarySort = getLongOrNull(_cursorIndexOfPrimarySort) ?: 0, secondarySort = getLongOrNull(_cursorIndexOfSecondarySort) ?: 0, - parentComplete = getBoolean(_cursorIndexOfParentComplete), + parentComplete = getBooleanOrNull(_cursorIndexOfParentComplete) ?: false, ) ) } @@ -171,3 +171,6 @@ private fun SQLiteStatement.getLongOrNull(index: Int): Long? = private fun SQLiteStatement.getIntOrNull(index: Int): Int? = if (index == -1 || isNull(index)) null else this.getInt(index) + +private fun SQLiteStatement.getBooleanOrNull(index: Int): Boolean? = + if (index == -1 || isNull(index)) null else this.getBoolean(index) 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 95e336330..eda7b5d65 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/CaldavDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/CaldavDao.kt @@ -4,13 +4,13 @@ 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 kotlinx.coroutines.flow.Flow 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,12 +22,13 @@ 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 { +abstract class CaldavDao(private val database: Database) { @Query("SELECT COUNT(*) FROM caldav_lists WHERE cdl_account = :account") abstract suspend fun listCount(account: String): Int @@ -98,24 +99,24 @@ ORDER BY CASE cda_account_type @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) + suspend fun insert(task: Task, caldavTask: CaldavTask, addToTop: Boolean): Long = + 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) + } else { + task.order = findLastTask(caldavTask.calendar!!, task.parent) ?.takeIf { task.creationDate.toAppleEpoch() <= it } ?.plus(1) + } + val id = insert(caldavTask) + update(task) + id } - val id = insert(caldavTask) - update(task) - return id - } @Query(""" SELECT MIN(IFNULL(`order`, (created - $APPLE_EPOCH) / 1000)) @@ -311,46 +312,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) - @Transaction - open suspend fun move( + 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) + 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) + } } + task.task.order = newPosition + setTaskOrder(task.id, 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() - 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 + suspend fun shiftDown(calendar: String, parent: Long, from: Long, to: Long? = null) { + 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 + } } - } - updateTasks(updated) - updated + 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/DeletionDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt index d6cefeeb9..b50282c27 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt @@ -3,15 +3,16 @@ package org.tasks.data.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Query -import androidx.room.Transaction import org.tasks.data.dao.CaldavDao.Companion.LOCAL -import org.tasks.data.entity.CaldavAccount -import org.tasks.data.entity.CaldavCalendar +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 { +abstract class DeletionDao(private val database: Database) { @Query("DELETE FROM tasks WHERE _id IN(:ids)") internal abstract suspend fun deleteTasks(ids: List) @@ -41,12 +42,7 @@ WHERE recurring = 1 """) abstract suspend fun internalHasRecurringAncestors(ids: List): List - @Transaction - open suspend fun delete(ids: List) { - ids.eachChunk { - deleteTasks(it) - } - } + suspend fun delete(ids: List) { ids.eachChunk { deleteTasks(it) } } @Query("UPDATE tasks " + "SET modified = (strftime('%s','now')*1000), deleted = (strftime('%s','now')*1000)" @@ -63,13 +59,13 @@ WHERE recurring = 1 @Delete internal abstract suspend fun deleteCaldavCalendar(caldavCalendar: CaldavCalendar) - @Transaction - open suspend fun delete(caldavCalendar: CaldavCalendar): List { - val tasks = getActiveCaldavTasks(caldavCalendar.uuid!!) - delete(tasks) - deleteCaldavCalendar(caldavCalendar) - return tasks - } + suspend fun delete(caldavCalendar: CaldavCalendar): List = + database.withTransaction { + val tasks = getActiveCaldavTasks(caldavCalendar.uuid!!) + delete(tasks) + deleteCaldavCalendar(caldavCalendar) + tasks + } @Query("SELECT * FROM caldav_lists WHERE cdl_account = :account") abstract suspend fun getCalendars(account: String): List @@ -80,13 +76,13 @@ 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() - @Transaction - open suspend fun delete(caldavAccount: CaldavAccount): List { - val deleted = ArrayList() - for (calendar in getCalendars(caldavAccount.uuid!!)) { - deleted.addAll(delete(calendar)) + suspend fun delete(caldavAccount: CaldavAccount): List = + database.withTransaction { + val deleted = ArrayList() + for (calendar in getCalendars(caldavAccount.uuid!!)) { + deleted.addAll(delete(calendar)) + } + deleteCaldavAccount(caldavAccount) + deleted } - deleteCaldavAccount(caldavAccount) - return deleted - } } \ 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 14da9879a..27a95de01 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/GoogleTaskDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/GoogleTaskDao.kt @@ -4,30 +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 org.tasks.data.entity.Task +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 { +abstract class GoogleTaskDao(private val database: Database) { @Insert abstract suspend fun insert(task: CaldavTask): Long @Insert abstract suspend fun insert(tasks: Iterable) - @Transaction - open suspend fun insertAndShift(task: Task, caldavTask: CaldavTask, top: Boolean) { - if (top) { - task.order = 0 - shiftDown(caldavTask.calendar!!, task.parent, 0) - } else { - task.order = getBottom(caldavTask.calendar!!, task.parent) + suspend fun insertAndShift(task: Task, caldavTask: CaldavTask, top: Boolean) { + 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) } - 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)") @@ -42,24 +44,25 @@ abstract class GoogleTaskDao { @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) - @Transaction - open suspend fun move(task: Task, list: String, newParent: Long, newPosition: Long) { - val previousParent = task.parent - val previousPosition = task.order!! - if (newParent == previousParent) { - if (previousPosition < newPosition) { - shiftUp(list, newParent, previousPosition, newPosition) + suspend fun move(task: Task, list: String, newParent: Long, newPosition: Long) { + 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) + } } else { - shiftDown(list, newParent, previousPosition, newPosition) + shiftUp(list, previousParent, previousPosition) + shiftDown(list, newParent, newPosition) } - } else { - shiftUp(list, previousParent, previousPosition) - shiftDown(list, newParent, newPosition) + task.parent = newParent + task.order = newPosition + update(task) + setMoved(task.id, list) } - 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") @@ -165,26 +168,27 @@ WHERE cd_remote_id = :id """) abstract suspend fun updatePosition(id: String, parent: String?, position: String) - @Transaction - open suspend fun reposition(caldavDao: CaldavDao, listId: String) { - 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) + suspend fun reposition(caldavDao: CaldavDao, listId: String) { + 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++ } - 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 c3094e648..6fc229383 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/PrincipalDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/PrincipalDao.kt @@ -4,39 +4,43 @@ 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 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 -interface PrincipalDao { +abstract class PrincipalDao(private val database: Database) { @Insert - suspend fun insert(principal: Principal): Long + abstract suspend fun insert(principal: Principal): Long @Insert - suspend fun insert(access: PrincipalAccess): Long + abstract suspend fun insert(access: PrincipalAccess): Long @Update - suspend fun update(access: PrincipalAccess) + abstract suspend fun update(access: PrincipalAccess) @Query(""" DELETE FROM principal_access WHERE list = :list AND id NOT IN (:access)""") - suspend fun deleteRemoved(list: Long, access: List) + abstract suspend fun deleteRemoved(list: Long, access: List) @Delete - suspend fun delete(access: PrincipalAccess) + abstract suspend fun delete(access: PrincipalAccess) + + suspend fun getAll(): List = database.withTransaction { + getAllInternal() + } - @Transaction @Query("SELECT * FROM principal_access") - suspend fun getAll(): List + internal abstract suspend fun getAllInternal(): List suspend fun getOrCreatePrincipal(account: CaldavAccount, href: String, displayName: String? = null) = findPrincipal(account.id, href) @@ -65,12 +69,12 @@ WHERE list = :list ).apply { id = insert(this) } @Query("SELECT * FROM principals WHERE account = :account AND href = :href") - suspend fun findPrincipal(account: Long, href: String): Principal? + abstract suspend fun findPrincipal(account: Long, href: String): Principal? @Query("SELECT * FROM principal_access WHERE list = :list and principal = :principal") - suspend fun findAccess(list: Long, principal: Long): PrincipalAccess? + abstract suspend fun findAccess(list: Long, principal: Long): PrincipalAccess? - @Transaction @Query("SELECT * FROM principal_access WHERE list = :id") - fun getPrincipals(id: Long): Flow> + abstract fun getPrincipals(id: Long): Flow> + } \ No newline at end of file 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 f9a5d5f0b..00bcd2e93 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/TagDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/TagDao.kt @@ -4,13 +4,14 @@ import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query -import androidx.room.Transaction +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 { +abstract class TagDao(private val database: Database) { @Query("UPDATE tags SET name = :name WHERE tag_uid = :tagUid") abstract suspend fun rename(tagUid: String, name: String) @@ -35,15 +36,16 @@ abstract class TagDao { @Delete abstract suspend fun delete(tags: List) - @Transaction open suspend fun applyTags(task: Task, tagDataDao: TagDataDao, current: List) { - val taskId = task.id - val existing = HashSet(tagDataDao.getTagDataForTask(taskId)) - val selected = HashSet(current) - val added = selected subtract existing - val removed = existing subtract selected - deleteTags(taskId, removed.map { td -> td.remoteId!! }) - insert(task, added) + database.withTransaction { + val taskId = task.id + val existing = HashSet(tagDataDao.getTagDataForTask(taskId)) + val selected = HashSet(current) + 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 62a68da29..2c4d0eb0f 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/TagDataDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/TagDataDao.kt @@ -4,19 +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 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 { +abstract class TagDataDao(private val database: Database) { @Query("SELECT * FROM tagdata") abstract fun subscribeToTags(): Flow> @@ -79,9 +80,11 @@ abstract class TagDataDao { + " GROUP BY tasks._id") internal abstract suspend fun getAllTags(tasks: List): List - @Transaction - open suspend fun applyTags( - tasks: List, partiallySelected: List, selected: List): List { + suspend fun applyTags( + tasks: List, + partiallySelected: List, + selected: List + ): List = database.withTransaction { val modified = HashSet() val keep = partiallySelected.plus(selected).map { it.remoteId!! } for (sublist in tasks.chunked(DbUtils.MAX_SQLITE_ARGS - keep.size)) { @@ -105,13 +108,14 @@ abstract class TagDataDao { ) } } - return ArrayList(modified) + ArrayList(modified) } - @Transaction - open suspend fun delete(tagData: TagData) { - deleteTags(tagData.remoteId!!) - deleteTagData(tagData) + suspend fun delete(tagData: TagData) { + database.withTransaction { + deleteTags(tagData.remoteId!!) + deleteTagData(tagData) + } } @Delete diff --git a/data/src/commonMain/kotlin/org/tasks/data/dao/TaskDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/TaskDao.kt index 3736d5c12..dff2291d8 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/TaskDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/TaskDao.kt @@ -6,7 +6,7 @@ import androidx.room.Query import androidx.room.Update import androidx.room.execSQL import co.touchlab.kermit.Logger -import org.tasks.data.BuildConfig +import org.tasks.IS_DEBUG import org.tasks.data.TaskContainer import org.tasks.data.UUIDHelper import org.tasks.data.db.Database @@ -111,7 +111,7 @@ FROM ( open suspend fun fetchTasks(callback: suspend () -> List): List = database.withTransaction { - val start = if (BuildConfig.DEBUG) DateTimeUtils2.currentTimeMillis() else 0 + val start = if (IS_DEBUG) DateTimeUtils2.currentTimeMillis() else 0 val queries = callback() val last = queries.size - 1 for (i in 0 until last) { @@ -205,7 +205,7 @@ FROM recursive_tasks if (Task.isUuidEmpty(task.remoteId)) { task.remoteId = UUIDHelper.newUUID() } - if (BuildConfig.DEBUG) { + if (IS_DEBUG) { require(task.remoteId?.isNotBlank() == true && task.remoteId != "0") } val insert = insert(task) diff --git a/data/src/commonMain/kotlin/org/tasks/data/entity/Alarm.kt b/data/src/commonMain/kotlin/org/tasks/data/entity/Alarm.kt index feaba28b8..798743c20 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/entity/Alarm.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/entity/Alarm.kt @@ -1,18 +1,18 @@ package org.tasks.data.entity -import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey -import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import org.tasks.CommonParcelable +import org.tasks.CommonParcelize import org.tasks.data.db.Table import org.tasks.time.printTimestamp import java.util.concurrent.TimeUnit -@Parcelize +@CommonParcelize @Serializable @Entity( tableName = Alarm.TABLE_NAME, @@ -41,7 +41,7 @@ data class Alarm( val repeat: Int = 0, @ColumnInfo(name = "interval", defaultValue = "0") val interval: Long = 0, -) : Parcelable { +) : CommonParcelable { fun same(other: Alarm) = type == other.type && time == other.time && diff --git a/data/src/commonMain/kotlin/org/tasks/data/entity/CaldavAccount.kt b/data/src/commonMain/kotlin/org/tasks/data/entity/CaldavAccount.kt index 374780768..1cd790941 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/entity/CaldavAccount.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/entity/CaldavAccount.kt @@ -1,17 +1,17 @@ package org.tasks.data.entity -import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import org.tasks.CommonParcelable +import org.tasks.CommonParcelize import org.tasks.data.db.Table import java.net.HttpURLConnection @Serializable -@Parcelize +@CommonParcelize @Entity(tableName = "caldav_accounts") data class CaldavAccount( @PrimaryKey(autoGenerate = true) @@ -38,7 +38,7 @@ data class CaldavAccount( val isCollapsed: Boolean = false, @ColumnInfo(name = "cda_server_type") var serverType: Int = SERVER_UNKNOWN, -) : Parcelable { +) : CommonParcelable { val isCaldavAccount: Boolean get() = accountType == TYPE_CALDAV diff --git a/data/src/commonMain/kotlin/org/tasks/data/entity/CaldavCalendar.kt b/data/src/commonMain/kotlin/org/tasks/data/entity/CaldavCalendar.kt index f189ddfe1..ec7eac246 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/entity/CaldavCalendar.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/entity/CaldavCalendar.kt @@ -1,18 +1,18 @@ package org.tasks.data.entity -import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import org.tasks.CommonParcelable +import org.tasks.CommonParcelize import org.tasks.data.LIST import org.tasks.data.NO_ORDER import org.tasks.data.db.Table @Serializable -@Parcelize +@CommonParcelize @Entity(tableName = "caldav_lists") data class CaldavCalendar( @PrimaryKey(autoGenerate = true) @@ -28,7 +28,7 @@ data class CaldavCalendar( @ColumnInfo(name = "cdl_order") val order: Int = NO_ORDER, @ColumnInfo(name = "cdl_access") var access: Int = ACCESS_OWNER, @ColumnInfo(name = "cdl_last_sync") val lastSync: Long = 0, -) : Parcelable { +) : CommonParcelable { @Suppress("RedundantNullableReturnType") fun getIcon(): Int? { return (if (icon == null) LIST else icon!!) diff --git a/data/src/commonMain/kotlin/org/tasks/data/entity/Filter.kt b/data/src/commonMain/kotlin/org/tasks/data/entity/Filter.kt index b342a8638..e5f2d9fda 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/entity/Filter.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/entity/Filter.kt @@ -1,16 +1,16 @@ package org.tasks.data.entity -import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import org.tasks.CommonParcelable +import org.tasks.CommonParcelize import org.tasks.data.NO_ORDER @Serializable -@Parcelize +@CommonParcelize @Entity(tableName = "filters") data class Filter( @PrimaryKey(autoGenerate = true) @@ -31,4 +31,4 @@ data class Filter( val icon: Int? = -1, @ColumnInfo(name = "f_order") val order: Int = NO_ORDER, -) : Parcelable +) : CommonParcelable diff --git a/data/src/commonMain/kotlin/org/tasks/data/entity/Task.kt b/data/src/commonMain/kotlin/org/tasks/data/entity/Task.kt index 2ed3a4301..9f9581d9f 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/entity/Task.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/entity/Task.kt @@ -1,6 +1,5 @@ package org.tasks.data.entity -import android.os.Parcelable import androidx.annotation.IntDef import androidx.room.ColumnInfo import androidx.room.Entity @@ -8,13 +7,14 @@ import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey import co.touchlab.kermit.Logger -import kotlinx.parcelize.Parcelize -import kotlinx.parcelize.RawValue import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.json.JsonNames +import org.tasks.CommonParcelable +import org.tasks.CommonParcelize +import org.tasks.CommonRawValue import org.tasks.data.db.Table import org.tasks.data.sql.Field @@ -22,7 +22,7 @@ const val SUPPRESS_SYNC = "suppress_sync" const val FORCE_CALDAV_SYNC = "force_caldav_sync" @Serializable -@Parcelize +@CommonParcelize @Entity( tableName = Task.TABLE_NAME, indices = [ @@ -82,8 +82,8 @@ data class Task @OptIn(ExperimentalSerializationApi::class) constructor( var readOnly: Boolean = false, @Ignore @Transient - private var transitoryData: @RawValue HashMap? = null, -) : Parcelable { + private var transitoryData: @CommonRawValue HashMap? = null, +) : CommonParcelable { var uuid: String get() = if (remoteId.isNullOrEmpty()) NO_UUID else remoteId!! set(uuid) { @@ -237,15 +237,8 @@ data class Task @OptIn(ExperimentalSerializationApi::class) constructor( return getTransitory(Tag.KEY) ?: ArrayList() } - fun setTags(tags: ArrayList) { - if (transitoryData == null) { - transitoryData = HashMap() - } - transitoryData!![Tag.KEY] = tags - } - fun hasTransitory(key: String?): Boolean { - return transitoryData != null && transitoryData!!.containsKey(key) + return transitoryData?.containsKey(key) == true } fun getTransitory(key: String?): T? = transitoryData?.get(key) as T? diff --git a/data/src/commonMain/kotlin/org/tasks/data/entity/TaskAttachment.kt b/data/src/commonMain/kotlin/org/tasks/data/entity/TaskAttachment.kt index 27f170ba1..ae9e87b3b 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/entity/TaskAttachment.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/entity/TaskAttachment.kt @@ -1,16 +1,16 @@ package org.tasks.data.entity -import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import org.tasks.CommonParcelable +import org.tasks.CommonParcelize import org.tasks.data.UUIDHelper @Serializable -@Parcelize +@CommonParcelize @Entity(tableName = "attachment_file") data class TaskAttachment( @PrimaryKey(autoGenerate = true) @@ -23,7 +23,7 @@ data class TaskAttachment( val name: String, @ColumnInfo(name = "uri") val uri: String, -) : Parcelable { +) : CommonParcelable { companion object { const val KEY = "attachment" const val FILES_DIRECTORY_DEFAULT = "attachments" diff --git a/data/src/commonMain/kotlin/org/tasks/data/entity/UserActivity.kt b/data/src/commonMain/kotlin/org/tasks/data/entity/UserActivity.kt index 5ea7c3f25..75338543d 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/entity/UserActivity.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/entity/UserActivity.kt @@ -1,112 +1,37 @@ package org.tasks.data.entity -import android.net.Uri -import android.os.Parcel -import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.Entity -import androidx.room.Ignore import androidx.room.PrimaryKey -import co.touchlab.kermit.Logger import kotlinx.serialization.Serializable import kotlinx.serialization.Transient -import org.json.JSONException -import org.json.JSONObject +import org.tasks.CommonParcelable +import org.tasks.CommonParcelize import org.tasks.data.db.Table -import java.io.File @Serializable +@CommonParcelize @Entity(tableName = "userActivity") -class UserActivity : Parcelable { +data class UserActivity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "_id") @Transient - var id: Long? = null - + var id: Long? = null, @ColumnInfo(name = "remoteId") - var remoteId: String? = Task.NO_UUID - + var remoteId: String? = Task.NO_UUID, @ColumnInfo(name = "message") - var message: String? = "" - + var message: String? = "", @ColumnInfo(name = "picture") - var picture: String? = "" - + var picture: String? = "", @ColumnInfo(name = "target_id") @Transient - var targetId: String? = Task.NO_UUID - + var targetId: String? = Task.NO_UUID, @ColumnInfo(name = "created_at") - var created: Long? = 0L - - constructor() - - @Ignore - private constructor(parcel: Parcel) { - with(parcel) { - id = readLong() - remoteId = readString() - message = readString() - picture = readString() - targetId = readString() - created = readLong() - } - } - - fun setPicture(uri: Uri?) { - picture = uri?.toString() - } - - val pictureUri: Uri? - get() = if (picture.isNullOrBlank()) null else Uri.parse(picture) - - fun convertPictureUri() { - setPicture(getLegacyPictureUri(picture)) - } - - override fun describeContents() = 0 - - override fun writeToParcel(dest: Parcel, flags: Int) { - with(dest) { - writeLong(id!!) - writeString(remoteId) - writeString(message) - writeString(picture) - writeString(targetId) - writeLong(created!!) - } - } - + var created: Long? = 0L, +) : CommonParcelable { companion object { @JvmField val TABLE = Table("userActivity") @JvmField val TASK = TABLE.column("target_id") @JvmField val MESSAGE = TABLE.column("message") - @JvmField val CREATOR: Parcelable.Creator = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): UserActivity = UserActivity(source) - - override fun newArray(size: Int): Array = arrayOfNulls(size) - } - - private fun getLegacyPictureUri(value: String?): Uri? { - return try { - if (value.isNullOrBlank()) { - return null - } - if (value.contains("uri") || value.contains("path")) { - val json = JSONObject(value) - if (json.has("uri")) { - return Uri.parse(json.getString("uri")) - } - if (json.has("path")) { - val path = json.getString("path") - return Uri.fromFile(File(path)) - } - } - null - } catch (e: JSONException) { - Logger.e("Failed to parse picture uri", e, tag = "UserActivity") - null - } - } } } \ No newline at end of file diff --git a/data/src/commonMain/kotlin/org/tasks/time/DateTimeUtils.kt b/data/src/commonMain/kotlin/org/tasks/time/DateTimeUtils.kt index 0cc6c5e75..fa84245fb 100644 --- a/data/src/commonMain/kotlin/org/tasks/time/DateTimeUtils.kt +++ b/data/src/commonMain/kotlin/org/tasks/time/DateTimeUtils.kt @@ -1,6 +1,6 @@ package org.tasks.time -import org.tasks.data.BuildConfig +import org.tasks.IS_DEBUG import java.util.Date object DateTimeUtils2 { @@ -24,4 +24,4 @@ object DateTimeUtils2 { } fun printTimestamp(timestamp: Long): String = - if (BuildConfig.DEBUG) Date(timestamp).toString() else timestamp.toString() \ No newline at end of file + if (IS_DEBUG) Date(timestamp).toString() else timestamp.toString() \ No newline at end of file diff --git a/data/src/jvmMain/kotlin/org/tasks/Platform.jvm.kt b/data/src/jvmMain/kotlin/org/tasks/Platform.jvm.kt new file mode 100644 index 000000000..1439942f5 --- /dev/null +++ b/data/src/jvmMain/kotlin/org/tasks/Platform.jvm.kt @@ -0,0 +1,13 @@ +@file:Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") + +package org.tasks + +actual interface CommonParcelable + +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.BINARY) +annotation class RawValue + +actual typealias CommonRawValue = RawValue + +actual val IS_DEBUG = false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd52843c0..223c34c69 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -164,3 +164,4 @@ kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = kotlin-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version = "2.0.0-1.0.21" } +jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }